Proof-of-concept Julian calendar

This commit is contained in:
Quantum 2023-04-22 19:02:25 -04:00
parent fc5f9b91b7
commit c307af7ca4
38 changed files with 34951 additions and 34 deletions

3
common/global-setup.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};

View file

@ -1,5 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'node', testEnvironment: 'jsdom',
globalSetup: './global-setup.js',
}; };

653
common/package-lock.json generated
View file

@ -23,6 +23,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"ts-jest": "^29.1.0" "ts-jest": "^29.1.0"
} }
}, },
@ -1130,6 +1131,15 @@
"@testing-library/dom": ">=7.21.4" "@testing-library/dom": ">=7.21.4"
} }
}, },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"dev": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@types/aria-query": { "node_modules/@types/aria-query": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
@ -1218,6 +1228,17 @@
"pretty-format": "^27.0.0" "pretty-format": "^27.0.0"
} }
}, },
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
"integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/tough-cookie": "*",
"parse5": "^7.0.0"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.18.24", "version": "16.18.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.24.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.24.tgz",
@ -1271,6 +1292,12 @@
"@types/jest": "*" "@types/jest": "*"
} }
}, },
"node_modules/@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
"dev": true
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.24", "version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
@ -1286,6 +1313,55 @@
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
"dev": true "dev": true
}, },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"dev": true
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-globals": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
"integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
"dev": true,
"dependencies": {
"acorn": "^8.1.0",
"acorn-walk": "^8.0.2"
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-escapes": { "node_modules/ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -1353,6 +1429,12 @@
"deep-equal": "^2.0.5" "deep-equal": "^2.0.5"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@ -1697,6 +1779,18 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1728,11 +1822,49 @@
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
}, },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"dev": true
},
"node_modules/cssstyle": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
"integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"dev": true,
"dependencies": {
"cssom": "~0.3.6"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cssstyle/node_modules/cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
}, },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
"integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
"dev": true,
"dependencies": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -1750,6 +1882,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
"dev": true
},
"node_modules/dedent": { "node_modules/dedent": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@ -1783,6 +1921,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@ -1807,6 +1951,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-newline": { "node_modules/detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -1829,6 +1982,18 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
}, },
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
"integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
"dev": true,
"dependencies": {
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.368", "version": "1.4.368",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.368.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.368.tgz",
@ -1853,6 +2018,18 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true "dev": true
}, },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -1898,6 +2075,28 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/escodegen": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
"integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
"dev": true,
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": { "node_modules/esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
@ -1911,6 +2110,24 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/execa": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -2045,6 +2262,12 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true "dev": true
}, },
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fb-watchman": { "node_modules/fb-watchman": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@ -2087,6 +2310,20 @@
"is-callable": "^1.1.3" "is-callable": "^1.1.3"
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2281,12 +2518,51 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"dev": true,
"dependencies": {
"whatwg-encoding": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true "dev": true
}, },
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"dev": true,
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -2296,6 +2572,18 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/import-local": { "node_modules/import-local": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@ -2507,6 +2795,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -3049,6 +3343,33 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true "dev": true
}, },
"node_modules/jest-environment-jsdom": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.5.0.tgz",
"integrity": "sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw==",
"dev": true,
"dependencies": {
"@jest/environment": "^29.5.0",
"@jest/fake-timers": "^29.5.0",
"@jest/types": "^29.5.0",
"@types/jsdom": "^20.0.0",
"@types/node": "*",
"jest-mock": "^29.5.0",
"jest-util": "^29.5.0",
"jsdom": "^20.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jest-environment-node": { "node_modules/jest-environment-node": {
"version": "29.5.0", "version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz",
@ -3658,6 +3979,51 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsdom": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
"integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
"dev": true,
"dependencies": {
"abab": "^2.0.6",
"acorn": "^8.8.1",
"acorn-globals": "^7.0.0",
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"data-urls": "^3.0.2",
"decimal.js": "^10.4.2",
"domexception": "^4.0.0",
"escodegen": "^2.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.2",
"parse5": "^7.1.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0",
"ws": "^8.11.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -3706,6 +4072,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dev": true,
"dependencies": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -3812,6 +4191,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": { "node_modules/mimic-fn": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@ -3886,6 +4286,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/nwsapi": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz",
"integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==",
"dev": true
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -3966,6 +4372,23 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"dependencies": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.6",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -4035,6 +4458,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"dev": true,
"dependencies": {
"entities": "^4.4.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4107,6 +4542,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@ -4144,6 +4588,21 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/pure-rand": { "node_modules/pure-rand": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz",
@ -4160,6 +4619,12 @@
} }
] ]
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"node_modules/react": { "node_modules/react": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
@ -4232,6 +4697,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.2", "version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@ -4279,6 +4750,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.20.2", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
@ -4512,6 +5001,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"node_modules/test-exclude": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@ -4553,6 +5048,33 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"dev": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dev": true,
"dependencies": {
"punycode": "^2.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ts-jest": { "node_modules/ts-jest": {
"version": "29.1.0", "version": "29.1.0",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz",
@ -4629,6 +5151,18 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
}, },
"node_modules/type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
"dev": true,
"dependencies": {
"prelude-ls": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/type-detect": { "node_modules/type-detect": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@ -4662,6 +5196,15 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
@ -4692,6 +5235,16 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
@ -4712,6 +5265,18 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true "dev": true
}, },
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
"integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
"dev": true,
"dependencies": {
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/walker": { "node_modules/walker": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -4726,6 +5291,49 @@
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
"integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="
}, },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dev": true,
"dependencies": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4789,6 +5397,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@ -4825,6 +5442,42 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -27,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"ts-jest": "^29.1.0" "ts-jest": "^29.1.0"
} }
} }

View file

@ -0,0 +1,36 @@
import {act, render} from '@testing-library/react';
import {DayChanger} from './DayChanger';
describe('Timezones', () => {
it('should always be UTC', () => {
expect(new Date().getTimezoneOffset()).toBe(0);
});
});
describe('DayChanger', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.clearAllMocks());
it('switches dates', () => {
const start = new Date('2023-04-22T17:16:00');
const end = new Date('2023-04-23T00:00:00');
const spy = jest
.spyOn(global, 'Date')
// @ts-ignore we are mocking the constructor but TypeScript thinks we are mocking the function
.mockImplementation(() => start);
const onChange = jest.fn();
render(<DayChanger onDateChange={onChange}/>);
act(() => {
jest.advanceTimersByTime(24_239_999);
expect(onChange).not.toHaveBeenCalled();
// @ts-ignore we are mocking the constructor but TypeScript thinks we are mocking the function
spy.mockImplementationOnce(() => end);
jest.advanceTimersByTime(1);
expect(onChange).toHaveBeenCalledWith(2460058);
});
});
});

26
common/src/DayChanger.tsx Normal file
View file

@ -0,0 +1,26 @@
import React from 'react';
import {dateJDN} from './gregorian';
function dayMs(date: Date): number {
return date.getHours() * 3_600_000 + date.getMinutes() * 60_000 + date.getSeconds() * 1000 + date.getMilliseconds();
}
type DayChangerProps = {
onDateChange: (jdn: number) => void,
};
export function DayChanger({onDateChange}: DayChangerProps): null {
const [jdn, setJDN] = React.useState(dateJDN(new Date()));
React.useEffect(() => {
const now = new Date();
const timer = setTimeout(() => {
const jdn = dateJDN(new Date());
setJDN(jdn);
onDateChange(jdn);
}, 86_400_000 - dayMs(now));
return () => clearTimeout(timer);
}, [jdn, onDateChange]);
return null;
}

View file

@ -42,9 +42,7 @@ export type FrenchDate = {
day: FrenchDay, day: FrenchDay,
}; };
const monthNames: { const monthNames: { [key in FrenchMonth]: string } = {
[key in FrenchMonth]: string
} = {
1: 'Vendémiaire', 1: 'Vendémiaire',
2: 'Brumaire', 2: 'Brumaire',
3: 'Frimaire', 3: 'Frimaire',
@ -64,8 +62,8 @@ export const decadeNames = [
'primidi', 'duodi', 'tridi', 'quartidi', 'quintidi', 'sextidi', 'septidi', 'octidi', 'nonidi', 'décadi', 'primidi', 'duodi', 'tridi', 'quartidi', 'quintidi', 'sextidi', 'septidi', 'octidi', 'nonidi', 'décadi',
]; ];
export const startJD = data.start_jd; export const frStartJD = data.start_jd;
export const startYear = data.start_year; export const frStartYear = data.start_year;
const leaps: Array<number> = [0]; const leaps: Array<number> = [0];
let leapFromStart = 0; let leapFromStart = 0;
@ -75,23 +73,23 @@ data.leap.forEach(leap => {
}); });
export const endYear = startYear + leaps.length - 1; export const frEndYear = frStartYear + leaps.length - 1;
export function frSupportedYear(year: number): boolean { export function frSupportedYear(year: number): boolean {
return startYear <= year && year <= endYear; return frStartYear <= year && year <= frEndYear;
} }
export function frJDN(year: number, month: FrenchMonth, day: FrenchDay): number { export function frJDN(year: number, month: FrenchMonth, day: FrenchDay): number {
const dy = year - startYear; const dy = year - frStartYear;
const dd = month * 30 + day - 31; const dd = month * 30 + day - 31;
return startJD + 365 * dy + leaps[dy] + dd; return frStartJD + 365 * dy + leaps[dy] + dd;
} }
export function frIsLeap(year: number): boolean { export function frIsLeap(year: number): boolean {
return !!data.leap[year - startYear]; return !!data.leap[year - frStartYear];
} }
export const endJD = frJDN(endYear, 13, frIsLeap(endYear) ? 6 : 5); export const frEndJD = frJDN(frEndYear, 13, frIsLeap(frEndYear) ? 6 : 5);
export function jdnFrench(jdn: number): FrenchDate { export function jdnFrench(jdn: number): FrenchDate {
let lo = 0; let lo = 0;
@ -99,18 +97,18 @@ export function jdnFrench(jdn: number): FrenchDate {
while (lo + 1 < hi) { while (lo + 1 < hi) {
const mid = Math.floor((lo + hi) / 2); const mid = Math.floor((lo + hi) / 2);
if (startJD + 365 * mid + leaps[mid] <= jdn) if (frStartJD + 365 * mid + leaps[mid] <= jdn)
lo = mid; lo = mid;
else else
hi = mid; hi = mid;
} }
const dd = jdn - (startJD + 365 * lo + leaps[lo]); const dd = jdn - (frStartJD + 365 * lo + leaps[lo]);
return { return {
year: startYear + lo, year: frStartYear + lo,
month: Math.floor(dd / 30) + 1 as FrenchMonth, month: Math.floor(dd / 30) + 1 as FrenchMonth,
day: dd % 30 + 1 as FrenchDay, day: dd % 30 + 1 as FrenchDay,
} };
} }
export function monthName(month: FrenchMonth): string { export function monthName(month: FrenchMonth): string {
@ -147,5 +145,9 @@ export function dateRuralName(month: FrenchMonth, day: FrenchDay): { name: strin
return {name, title}; return {name, title};
} }
export const startGregorian = jdnGregorian(startJD); export const startGregorian = jdnGregorian(frStartJD);
export const endGregorian = jdnGregorian(endJD); export const endGregorian = jdnGregorian(frEndJD);
export function frDateFormat({year, month, day}: { year: number, month: FrenchMonth, day: FrenchDay }): string {
return `${dateName(month, day)} ${year}`;
}

View file

@ -1,4 +1,4 @@
import {gregorianJDN, jdnDate, jdnGregorian, JulianDay, JulianMonth} from './gregorian'; import {gregorianJDN, jdnDate, jdnGregorian, JulianDay, JulianMonth, gregorianMonthDays} from './gregorian';
describe('gregorianJDN', () => { describe('gregorianJDN', () => {
it('works', () => { it('works', () => {
@ -67,3 +67,42 @@ describe('jdnGregorian', () => {
checkJDN(0, -4712, 1, 1); checkJDN(0, -4712, 1, 1);
}); });
}); });
describe('monthLength', () => {
it('works for normal months', () => {
expect(gregorianMonthDays(2023, 1)).toEqual(31); // January
expect(gregorianMonthDays(2023, 3)).toEqual(31); // March
expect(gregorianMonthDays(2023, 4)).toEqual(30); // April
expect(gregorianMonthDays(2023, 5)).toEqual(31); // May
expect(gregorianMonthDays(2023, 6)).toEqual(30); // June
expect(gregorianMonthDays(2023, 7)).toEqual(31); // July
expect(gregorianMonthDays(2023, 8)).toEqual(31); // August
expect(gregorianMonthDays(2023, 9)).toEqual(30); // September
expect(gregorianMonthDays(2023, 10)).toEqual(31); // October
expect(gregorianMonthDays(2023, 11)).toEqual(30); // November
expect(gregorianMonthDays(2023, 12)).toEqual(31); // December
});
it('handles Gregorian leap years correctly', () => {
// Leap year: divisible by 400
expect(gregorianMonthDays(1600, 2)).toEqual(29);
expect(gregorianMonthDays(2000, 2)).toEqual(29);
expect(gregorianMonthDays(2400, 2)).toEqual(29);
// Not a leap year: divisible by 100 but not by 400
expect(gregorianMonthDays(1700, 2)).toEqual(28);
expect(gregorianMonthDays(1800, 2)).toEqual(28);
expect(gregorianMonthDays(1900, 2)).toEqual(28);
expect(gregorianMonthDays(2100, 2)).toEqual(28);
// Leap year: divisible by 4 but not by 100
expect(gregorianMonthDays(2004, 2)).toEqual(29);
expect(gregorianMonthDays(2008, 2)).toEqual(29);
expect(gregorianMonthDays(2012, 2)).toEqual(29);
expect(gregorianMonthDays(2016, 2)).toEqual(29);
expect(gregorianMonthDays(2001, 2)).toEqual(28);
expect(gregorianMonthDays(2002, 2)).toEqual(28);
expect(gregorianMonthDays(2003, 2)).toEqual(28);
});
});

View file

@ -34,6 +34,25 @@ export type JulianDay =
export type JulianDate = [number, JulianMonth, JulianDay]; export type JulianDate = [number, JulianMonth, JulianDay];
const monthNames: { [key in JulianMonth]: string } = {
1: 'January',
2: 'February',
3: 'March',
4: 'April',
5: 'May',
6: 'June',
7: 'July',
8: 'August',
9: 'September',
10: 'October',
11: 'November',
12: 'December',
};
export const weekdayNames = [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
];
export function gregorianJDN(year: number, month: number, day: number, julian_before?: number): number { export function gregorianJDN(year: number, month: number, day: number, julian_before?: number): number {
const g = year + 4716 - (month <= 2 ? 1 : 0); const g = year + 4716 - (month <= 2 ? 1 : 0);
const f = (month + 9) % 12; const f = (month + 9) % 12;
@ -67,3 +86,36 @@ export function jdnDate(jdn: number, julian_before?: number): Date {
const [year, month, day] = jdnGregorian(jdn, julian_before); const [year, month, day] = jdnGregorian(jdn, julian_before);
return new Date(year, month - 1, day); return new Date(year, month - 1, day);
} }
export function monthName(month: JulianMonth): string {
return monthNames[month];
}
export function gregorianMonthDays(year: number, month: JulianMonth, julian = false): 28 | 29 | 30 | 31 {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
if (year % 4 !== 0)
return 28;
if (!julian && (year % 100 === 0 && year % 400 !== 0))
return 28;
return 29;
}
}
export function formatJG([year, month, day]: [number, JulianMonth, JulianDay]): string {
const m = monthNames[month].substring(0, 3);
return `${m} ${day} ${year}`;
}

View file

@ -1,4 +1,4 @@
import {jdnJulian, julianJDN} from './julian'; import {jdnJulian, julianJDN, julianMonthDays} from './julian';
describe('gregorianJDN', () => { describe('gregorianJDN', () => {
it('works', () => { it('works', () => {
@ -23,3 +23,42 @@ describe('jdnGregorian', () => {
expect(jdnJulian(0)).toEqual([-4712, 1, 1]); expect(jdnJulian(0)).toEqual([-4712, 1, 1]);
}); });
}); });
describe('monthLength', () => {
it('works for normal months', () => {
expect(julianMonthDays(2023, 1)).toEqual(31); // January
expect(julianMonthDays(2023, 3)).toEqual(31); // March
expect(julianMonthDays(2023, 4)).toEqual(30); // April
expect(julianMonthDays(2023, 5)).toEqual(31); // May
expect(julianMonthDays(2023, 6)).toEqual(30); // June
expect(julianMonthDays(2023, 7)).toEqual(31); // July
expect(julianMonthDays(2023, 8)).toEqual(31); // August
expect(julianMonthDays(2023, 9)).toEqual(30); // September
expect(julianMonthDays(2023, 10)).toEqual(31); // October
expect(julianMonthDays(2023, 11)).toEqual(30); // November
expect(julianMonthDays(2023, 12)).toEqual(31); // December
});
it('handles Gregorian leap years correctly', () => {
// Leap year: divisible by 400
expect(julianMonthDays(1600, 2)).toEqual(29);
expect(julianMonthDays(2000, 2)).toEqual(29);
expect(julianMonthDays(2400, 2)).toEqual(29);
// Leap year in Julian calendar: divisible by 100 but not by 400
expect(julianMonthDays(1700, 2)).toEqual(29);
expect(julianMonthDays(1800, 2)).toEqual(29);
expect(julianMonthDays(1900, 2)).toEqual(29);
expect(julianMonthDays(2100, 2)).toEqual(29);
// Leap year: divisible by 4 but not by 100
expect(julianMonthDays(2004, 2)).toEqual(29);
expect(julianMonthDays(2008, 2)).toEqual(29);
expect(julianMonthDays(2012, 2)).toEqual(29);
expect(julianMonthDays(2016, 2)).toEqual(29);
expect(julianMonthDays(2001, 2)).toEqual(28);
expect(julianMonthDays(2002, 2)).toEqual(28);
expect(julianMonthDays(2003, 2)).toEqual(28);
});
});

View file

@ -1,4 +1,4 @@
import {gregorianJDN, jdnGregorian, JulianDate} from './gregorian'; import {gregorianJDN, jdnGregorian, gregorianMonthDays, JulianDate, JulianMonth} from './gregorian';
export function julianJDN(year: number, month: number, day: number): number { export function julianJDN(year: number, month: number, day: number): number {
return gregorianJDN(year, month, day, Infinity); return gregorianJDN(year, month, day, Infinity);
@ -7,3 +7,7 @@ export function julianJDN(year: number, month: number, day: number): number {
export function jdnJulian(jdn: number): JulianDate { export function jdnJulian(jdn: number): JulianDate {
return jdnGregorian(jdn, Infinity); return jdnGregorian(jdn, Infinity);
} }
export function julianMonthDays(year: number, month: JulianMonth): 28 | 29 | 30 | 31 {
return gregorianMonthDays(year, month, true);
}

View file

@ -10,7 +10,7 @@
"@types/node": "^16.11.24", "@types/node": "^16.11.24",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"bootstrap": "^5.1.3", "bootstrap": "~5.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",

View file

@ -2,12 +2,12 @@ import React, {FormEvent} from 'react';
import {Calendar} from './Calendar'; import {Calendar} from './Calendar';
import { import {
endGregorian, endGregorian,
endJD, frEndJD,
FrenchMonth, FrenchMonth,
frSupportedYear, frSupportedYear,
jdnFrench, jdnFrench,
startGregorian, startGregorian,
startJD, frStartJD,
} from '@common/french'; } from '@common/french';
import {dateJDN, gregorianJDN} from '@common/gregorian'; import {dateJDN, gregorianJDN} from '@common/gregorian';
import {TimeOfDay} from './TimeOfDay'; import {TimeOfDay} from './TimeOfDay';
@ -99,7 +99,7 @@ class App extends React.Component<{}, AppState> {
return; return;
const jdn = gregorianJDN(+this.state.goYear, +this.state.goMonth, +this.state.goDay); const jdn = gregorianJDN(+this.state.goYear, +this.state.goMonth, +this.state.goDay);
const {year, month} = jdnFrench(Math.min(Math.max(startJD, jdn), endJD)); const {year, month} = jdnFrench(Math.min(Math.max(frStartJD, jdn), frEndJD));
this.setState({year, month}); this.setState({year, month});
} }

View file

@ -4,14 +4,14 @@ import {
dateName, dateName,
dateRuralName, dateRuralName,
decadeNames, decadeNames,
endYear, frEndYear,
FrenchDay, FrenchDay,
FrenchMonth, FrenchMonth,
frIsLeap, frIsLeap,
frJDN, frJDN,
jdnFrench, jdnFrench,
monthName, monthName,
startYear, frStartYear,
} from '@common/french'; } from '@common/french';
import {jdnDate} from '@common/gregorian'; import {jdnDate} from '@common/gregorian';
import {jdnLongCount} from '@common/longCount'; import {jdnLongCount} from '@common/longCount';
@ -123,11 +123,11 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
month -= 13; month -= 13;
} }
if (year < startYear) { if (year < frStartYear) {
year = startYear; year = frStartYear;
month = 1; month = 1;
} else if (year > endYear) { } else if (year > frEndYear) {
year = endYear; year = frEndYear;
month = 13; month = 13;
} }
@ -215,7 +215,7 @@ export class Calendar extends React.Component<CalendarProps, CalendarState> {
}) })
}</select> }</select>
<input type="number" className="Calendar-year-input form-control" value={this.state.yearStr} <input type="number" className="Calendar-year-input form-control" value={this.state.yearStr}
onChange={this.yearChange} min={startYear} max={endYear}/> onChange={this.yearChange} min={frStartYear} max={frEndYear}/>
<button type="button" className="form-control btn btn-primary Calendar-today-button" <button type="button" className="form-control btn btn-primary Calendar-today-button"
onClick={this.goToToday}>Today onClick={this.goToToday}>Today
</button> </button>

File diff suppressed because one or more lines are too long

5
jcal/config-overrides.js Normal file
View file

@ -0,0 +1,5 @@
const {aliasWebpack, aliasJest} = require('react-app-alias-ex');
const options = {};
module.exports = aliasWebpack(options);
module.exports.jest = aliasJest(options);

15587
jcal/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

51
jcal/package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "jcal",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.0",
"@types/node": "^16.11.24",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"bootstrap": "~5.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"sass": "^1.49.7",
"sass-loader": "^12.4.0",
"typescript": "^4.5.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/bootstrap": "^5.1.9",
"react-app-alias-ex": "^2.1.0",
"react-app-rewired": "^2.2.1"
}
}

BIN
jcal/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

138
jcal/public/index.html Normal file
View file

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#ff0000"/>
<meta name="description"
content="An interactive French Republican Calendar (a.k.a. French Revolutionary Calendar) that uses the original equinox method and never drifts out of sync with the seasons, along with revolutionary decimal time."/>
<meta property="og:title" content="French Republican Calendar"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://frcal.qt.ax/"/>
<meta property="og:description"
content="An interactive French Republican Calendar (a.k.a. French Revolutionary Calendar) that uses the original equinox method and never drifts out of sync with the seasons, along with revolutionary decimal time."/>
<meta property="og:image" content="%PUBLIC_URL%/logo512.png"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="512"/>
<meta property="og:image:height" content="512"/>
<meta property="og:image:alt" content="A calendar icon that displays the date 18 Brumaire."/>
<link rel="canonical" href="%PUBLIC_URL%/"/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<title>Julian Calendar (extended indefinitely to the future)</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-light">
<div class="container">
<a class="navbar-brand" href="#">Julian Calendar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav-anchors"
aria-controls="nav-anchors" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav-anchors">
<ul class="nav navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#explanation">Explanation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://frcal.qt.ax">French Republican Calendar</a>
</li>
</ul>
</div>
</div>
</nav>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div class="main">
<h2 id="explanation">Explanation</h2>
<div class="card">
<div class="card-body">
<h4 class="card-title">What is this?</h4>
<p class="lead">The <a href="https://en.wikipedia.org/wiki/Julian_calendar">Julian
calendar</a> is a calendar introduced by Julius Caesar in 46 BC. A reformed version, the <a
href="https://en.wikipedia.org/wiki/Gregorian_calendar">Gregorian
calendar</a>, forms the basis of civil calendar in most countries.</p>
<p>Julius Caesar (with aid of Sosigenes of Alexandria) introduced this calendar to replace the earlier Roman
calendar, which required manual addition of leap months to keep it in synchrony with the
seasons.Unfortunately, it had leap days every four years without question, resulting in the average year
having 365.25 days. However, the actual tropical year is roughly 365.2422 days, resulting in the Julian
calendar gaining a day every 129 years. This means that seasons drift, starting earlier and earlier in
the year.</p>
<p>In 1582, Pope Gregory XIII decided to fix this drift by reducing the number of leap days, motivated by a
desire to keep the March equinox on March 21st, since that value was hardcoded in the calculation for
the date of Easter. To achieve this, he made years divisible by 100 but not by 400 non-leap years,
resulting in 97 leap years every 400 years, resulting in the Gregorian calendar. To bring the equinox
back in alignment, October 5th to October 14th in 1582 were deleted. The result is the Gregorian
calendar.</p>
<p>This website extends the Julian calendar indefinitely into the future for reference reasons. Note that in
the 20th and 21st centuries, the Julian calendar is 13 days behind the Gregorian calendar.</p>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title">How are BCE years handled?</h4>
<p class="lead">The astronomical convention is used, i.e. year 0 is 1 BCE, year -1 is 2 BCE, etc.</p>
<p>Due to the way the common era (a.k.a. Anno Domini) year numbering system works, the year 1 BCE is
followed directly by 1 CE with no year zero. This makes math hard. As such, the astronomical convention
of making 1 BCE year 0 and extending this into the past was used.</p>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title">What is the JD (Julian day number) value?</h4>
<p>The integer <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day (JD) number</a> is the
continuous count of days since the beginning of the Julian period, which is very useful in astronomy and
in software for calculating durations without worrying about weird calendar issues. In this respect,
it's very similar to Unix time.</p>
<p>JD 0 is assigned to the date Monday, January 1, 4713 BCE (Julian) or November 24, 4714 BCE (Gregorian).
More specifically, if fractional JDs are considered, then the integer value specifically refers to the
Universal Time noon on that date.</p>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title">What is the LC (Mesoamerican Long Count) date?</h4>
<p>The five (or more) numbers separated by dots is the corresponding
<a href="https://en.wikipedia.org/wiki/Mesoamerican_Long_Count_calendar">Mesoamerican Long Count
calendar</a> date. This is commonly known as the &ldquo;Mayan calendar.&rdquo; This calendar is not
available for dates before August 11, 3114 BCE (Gregorian) or September 6, 3114 BCE (Julian).</p>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title">What is the FR (French Republican calendar) date?</h4>
<p>The <a href="https://en.wikipedia.org/wiki/French_Republican_calendar">French Republican
calendar</a> was a calendar created and implemented during the French Revolution. It is also frequently
referred to as the <em>French Revolutionary Calendar</em>, but this is a misnomer:
year 1 of the calendar started on 22 September 1792, the day after the
<a href="https://en.wikipedia.org/wiki/Proclamation_of_the_abolition_of_the_monarchy">abolition of the
monarchy</a> and the founding of the <a href="https://en.wikipedia.org/wiki/French_First_Republic">French
First Republic</a>.</p>
<p>Specifically, the variant used here is <a href="https://frcal.qt.ax/">the one I computed from
astronomy</a>.</p>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">Copyright &copy; 2022<%= new Date().getFullYear() > 2022 ? `${new Date().getFullYear()}`
: '' %>
<a href="https://quantum5.ca">Quantum</a>.
Licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">GNU AGPLv3</a>.
Source code available on <a href="https://github.com/quantum5/frcal">GitHub</a>.<br>
</p>
</div>
</footer>
<script>
(function(j,u,l,i,a,n,c){j['GoogleAnalyticsObject']=a;j[a]=j[a]||function(){
(j[a].q=j[a].q||[]).push(arguments)},j[a].l=1*new Date();n=u.createElement(l),
c=u.getElementsByTagName(l)[0];n.async=1;n.src=i;c.parentNode.insertBefore(n,c)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-102581070-4', 'auto');
ga('send', 'pageview');
</script>
</body>
</html>

70
jcal/public/logo.svg Normal file
View file

@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" viewBox="0 0 87.6 98.5">
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="#d0bcba"/>
<stop offset="1" stop-color="#fefdfd"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#d0bcba"/>
<stop offset="1" stop-color="#6c6c6c"/>
</linearGradient>
<linearGradient id="b">
<stop offset="0" stop-color="#978888"/>
<stop offset="1" stop-color="#fbfcfc"/>
</linearGradient>
<linearGradient id="d" x1="-33.8" x2="14.2" y1="36.5" y2="50.5" gradientTransform="translate(178.8 49.4)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#be9e70"/>
<stop offset="1" stop-color="#694c30"/>
</linearGradient>
<linearGradient id="e" x1="195.9" x2="196.2" y1="89" y2="70.7" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#e40102"/>
<stop offset="1" stop-color="#bb7874"/>
</linearGradient>
<linearGradient id="f" x1="195.9" x2="195.9" y1="73" y2="103.6" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7c1f16"/>
<stop offset="1" stop-color="#7c1f16" stop-opacity="0"/>
</linearGradient>
<linearGradient id="g" x1="197.6" x2="196.7" y1="81.1" y2="88.7" gradientTransform="translate(0 .2)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#aa090a"/>
<stop offset="1" stop-color="#e50102"/>
</linearGradient>
<linearGradient xlink:href="#a" id="i" x1="281.2" x2="279.6" y1="94" y2="92.5" gradientTransform="translate(-34.4 9.2)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#a" id="j" x1="213.3" x2="223.5" y1="113.9" y2="122.7" gradientTransform="translate(29.5 -14)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="k" x1="221.6" x2="219.2" y1="35" y2="35" gradientTransform="matrix(1.078 0 0 1.0746 -55.1 33.9)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="l" x1="250.1" x2="250.1" y1="38.6" y2="26.5" gradientTransform="translate(-68 39)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="m" x1="221.6" x2="219.2" y1="35" y2="35" gradientTransform="matrix(1.078 0 0 1.0746 -47.6 33.9)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="n" x1="250.1" x2="250.1" y1="38.6" y2="26.5" gradientTransform="translate(-60.5 39)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="o" x1="221.6" x2="219.2" y1="35" y2="35" gradientTransform="matrix(1.078 0 0 1.0746 -40 33.9)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="p" x1="250.1" x2="250.1" y1="38.6" y2="26.5" gradientTransform="translate(-53 39)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="q" x1="221.6" x2="219.2" y1="35" y2="35" gradientTransform="matrix(1.078 0 0 1.0746 -32.6 33.9)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="r" x1="250.1" x2="250.1" y1="38.6" y2="26.5" gradientTransform="translate(-45.5 39)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="s" x1="221.6" x2="219.2" y1="35" y2="35" gradientTransform="matrix(1.078 0 0 1.0746 -25 33.9)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="t" x1="250.1" x2="250.1" y1="38.6" y2="26.5" gradientTransform="translate(-38 39)" gradientUnits="userSpaceOnUse"/>
<radialGradient id="h" cx="196.9" cy="132.3" r="21" gradientTransform="matrix(0 .98878 -1.0116 0 343.8 -116.1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fffefe"/>
<stop offset="1" stop-color="#d5d7da"/>
</radialGradient>
</defs>
<g transform="matrix(1.78746 0 0 1.85973 -259.2 -121.6)">
<rect width="48" height="48" x="146" y="70.4" fill="#694c30" rx="1.6" ry="1.5"/>
<rect width="48" height="48" x="145" y="69.4" fill="url(#d)" rx="1.6" ry="1.5"/>
</g>
<path fill="url(#e)" stroke="url(#f)" stroke-linecap="round" stroke-width=".6" d="M177 73.3h41.4v15.4H177z" transform="matrix(1.8247 0 0 1.82257 -316.8 -121.9)"/>
<path fill="url(#g)" d="M177 74v14.5h41.2V86c-10.5-.1-14-2.4-18.8-4.4-4.3-1.8-13.5-.1-17.8-1.3-3.2-1.4-3-2.7-4.5-6.2z" transform="matrix(1.8247 0 0 1.82257 -316.8 -121.9)"/>
<path fill="url(#h)" stroke="#afa497" stroke-linecap="round" stroke-width=".6" d="M206.2 75.1h41.4v29.4h-41.4z" transform="translate(-370.2 -96.5) scale(1.8247)"/>
<path fill="none" stroke="#c60002" stroke-opacity=".1" d="M227.4 90.9h41.2m-41.2 4h41.2M227.4 115h41.2m-41.2-4h41.2m-41.2-4.1h41.2m-41.2-4h41.2m-41.2-4h41.2" transform="matrix(1.8247 0 0 1.82257 -408.3 -119.2)"/>
<path fill="url(#i)" fill-rule="evenodd" stroke="url(#j)" stroke-width=".5" d="M247.5 97.6a16 16 0 0 1-5.7 6.7c2.1.3 4-.5 5-1 .4-1.6.7-4 .7-5.7z" opacity=".5" transform="translate(-370.2 -96.5) scale(1.8247)"/>
<g stroke-linecap="round" stroke-width=".7" transform="matrix(1.8247 0 0 1.82257 -315.8 -119.3)">
<rect width="2.9" height="11.4" x="180.6" y="65.8" fill="url(#k)" stroke="url(#l)" rx="1.4" ry="2.2"/>
<rect width="2.9" height="11.4" x="188.1" y="65.8" fill="url(#m)" stroke="url(#n)" rx="1.4" ry="2.2"/>
<rect width="2.9" height="11.4" x="195.7" y="65.8" fill="url(#o)" stroke="url(#p)" rx="1.4" ry="2.2"/>
<rect width="2.9" height="11.4" x="203.2" y="65.8" fill="url(#q)" stroke="url(#r)" rx="1.4" ry="2.2"/>
<rect width="2.9" height="11.4" x="210.7" y="65.8" fill="url(#s)" stroke="url(#t)" rx="1.4" ry="2.2"/>
</g>
<g style="line-height:1.25" word-spacing="0">
<path fill="#fff" d="M15.2 24.5h4.2l2.8 6.7 3-6.7h4V37h-3v-9.2l-3 6.8h-2l-2.9-6.8V37h-3zm21.4 8.3q-1 0-1.4.4-.5.3-.5.9 0 .6.4 1 .3.2 1 .2.9 0 1.5-.6t.6-1.5v-.4zm4.6-1.1V37h-3v-1.4q-.7.9-1.4 1.3-.8.3-1.9.3-1.4 0-2.4-.8-.9-.9-.9-2.2 0-1.7 1.2-2.5 1.1-.8 3.6-.8h1.8v-.2q0-.7-.6-1-.6-.4-1.8-.4-1 0-1.8.2t-1.5.6v-2.3l2-.4 1.9-.1q2.5 0 3.7 1 1.1 1 1.1 3.3zm9.8-1.5-.7-.3h-.8q-1.2 0-1.8.7-.6.7-.6 2.1v4.4h-3v-9.5h3v1.6q.5-1 1.3-1.4.8-.4 1.8-.4h.9zm9.2-2.2v2.4l-1.2-.6q-.6-.2-1.3-.2-1.3 0-2 .7t-.7 2q0 1.4.7 2.1.7.8 2 .8l1.4-.2q.6-.2 1.1-.7v2.5l-1.4.4-1.5.1q-2.5 0-4-1.3-1.4-1.3-1.4-3.6 0-2.4 1.4-3.7 1.5-1.3 4-1.3l1.5.1 1.4.4zm12.2 3.3v5.8h-3v-4.4l-.1-1.7q0-.5-.2-.7l-.5-.5-.7-.1q-1 0-1.5.7t-.5 2v4.7h-3V23.9h3V29q.7-.8 1.4-1.2.8-.4 1.7-.4 1.7 0 2.5 1 .9 1 .9 3z" aria-label="March"/>
</g>
<g style="line-height:1.25" word-spacing="0">
<path d="M24.6 74.7h5.3V59.6l-5.4 1.1v-4l5.4-1.2h5.7v19.2h5.3v4.1H24.6zm21.9-19.2h15V60H51.3v3.6l1.4-.2 1.4-.1q4.3 0 6.7 2 2.3 2.2 2.3 6t-2.6 6q-2.5 2-7.1 2-2 0-4-.3-1.9-.4-3.8-1.2V73q1.9 1.1 3.6 1.7 1.7.5 3.2.5 2.2 0 3.5-1 1.2-1.1 1.2-3 0-1.8-1.2-2.9-1.3-1-3.5-1-1.3 0-2.7.3-1.5.3-3.2 1z" aria-label="15"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
jcal/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
jcal/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

25
jcal/public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "French Republican Calendar",
"name": "French Republican Calendar",
"icons": [
{
"src": "favicon.ico",
"sizes": "128x128 64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ff0000",
"background_color": "#ffffff"
}

3
jcal/public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

143
jcal/src/App.tsx Normal file
View file

@ -0,0 +1,143 @@
import React, {FormEvent} from 'react';
import {Calendar} from './Calendar';
import {frEndJD, frStartJD} from '@common/french';
import {dateJDN, gregorianJDN, JulianMonth} from '@common/gregorian';
import {DayChanger} from '@common/DayChanger';
import {jdnJulian} from '@common/julian';
// Not real limitations other than JS number precision.
const START_YEAR = -10_000_000_000_000;
const END_YEAR = 10_000_000_000_000;
type YearMonth = {
year: number;
month: JulianMonth;
}
function parseURL(): YearMonth | null {
const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname);
if (!match)
return null;
const month = +match[2];
const year = +match[1];
if (month < 1 || month > 23)
return null;
return {year: year, month: month as JulianMonth};
}
type AppState = YearMonth & {
todayJDN: number,
goYear: string,
goMonth: string,
goDay: string,
};
class App extends React.Component<{}, AppState> {
state: AppState;
constructor(props: {}) {
super(props);
const today = new Date();
const todayJDN = dateJDN(today);
const [year, month] = jdnJulian(todayJDN);
this.state = {
...(parseURL() || {year, month}),
todayJDN,
goYear: today.getFullYear().toString(),
goMonth: (today.getMonth() + 1).toString(),
goDay: today.getDate().toString(),
};
this.updateStateFromURL = this.updateStateFromURL.bind(this);
}
componentDidMount() {
window.addEventListener('popstate', this.updateStateFromURL);
}
componentWillUnmount() {
window.removeEventListener('popstate', this.updateStateFromURL);
}
private updateStateFromURL(event: PopStateEvent) {
this.setState(event.state);
}
private updateURL() {
const {year, month} = this.state;
const path = `/${year}/${month}`;
if (path !== window.location.pathname) {
window.history.pushState({year, month}, '', path);
}
}
changeField(field: keyof AppState, event: any) {
this.setState({[field]: event.target.value});
}
validYear() {
return /^-?\d+$/.test(this.state.goYear) && START_YEAR <= +this.state.goYear && +this.state.goYear <= END_YEAR;
}
validMonth() {
return /^\d+$/.test(this.state.goMonth) && 1 <= +this.state.goMonth && +this.state.goMonth <= 12;
}
validDay() {
return /^\d+$/.test(this.state.goDay) && 1 <= +this.state.goDay && +this.state.goDay <= 31;
}
goToGregorian(event: FormEvent) {
event.preventDefault();
if (!this.validYear() || !this.validMonth() || !this.validDay())
return;
const jdn = gregorianJDN(+this.state.goYear, +this.state.goMonth, +this.state.goDay);
const [year, month] = jdnJulian(Math.min(Math.max(frStartJD, jdn), frEndJD));
this.setState({year, month});
}
setState(state: any, callback?: () => void) {
super.setState(state, () => {
this.updateURL();
callback?.();
});
}
onDateChange = (todayJDN: number) => {
this.setState({todayJDN});
};
render() {
return <>
<Calendar
year={this.state.year} month={this.state.month} todayJDN={this.state.todayJDN}
onSwitch={(year, month) => {
this.setState({year, month});
}}/>
<DayChanger onDateChange={this.onDateChange}/>
<div className="navigate">
<h4>Go to a date</h4>
<form className="input-group" onSubmit={this.goToGregorian.bind(this)}>
<span className="input-group-text">Gregorian<span className="hide-small">&nbsp;Date</span></span>
<input type="number" className={`form-control go-year ${this.validYear() ? '' : 'is-invalid'}`}
onChange={this.changeField.bind(this, 'goYear')} value={this.state.goYear}
min={START_YEAR} max={END_YEAR}/>
<input type="number" className={`form-control go-month ${this.validMonth() ? '' : 'is-invalid'}`}
onChange={this.changeField.bind(this, 'goMonth')} value={this.state.goMonth}
min={1} max={12}/>
<input type="number" className={`form-control go-day ${this.validDay() ? '' : 'is-invalid'}`}
onChange={this.changeField.bind(this, 'goDay')} value={this.state.goDay}
min={1} max={31}/>
<button type="submit" className="form-control btn btn-primary go-button">Go</button>
</form>
</div>
</>;
}
}
export default App;

164
jcal/src/Calendar.scss Normal file
View file

@ -0,0 +1,164 @@
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/forms';
@import 'bootstrap/scss/grid';
@import 'bootstrap/scss/buttons';
@import './consts';
.Calendar {
@include make-container();
}
.Calendar-head {
display: flex;
justify-content: center;
font-size: 2em;
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
background: white;
margin-top: -$spacer * 4;
padding-top: $spacer * 4;
.btn {
margin: 0 1px;
}
}
.Calendar-prev, .Calendar-next {
flex: 0 2.5em;
}
.Calendar-prev {
text-align: right;
}
.Calendar-month-name {
flex: 1;
max-width: 30rem;
text-align: center;
}
.Month-weekdayHead {
display: none;
}
.Month-days {
@include make-row($gutter: 0);
> * {
@include make-col-ready();
}
}
.DayOuter {
padding: $calendar-gutter;
}
.DayFiller {
display: none;
}
.DayOuter, .WeekdayName {
display: flex;
@include make-col($size: 1, $columns: 2);
}
@include media-breakpoint-up(sm) {
.DayOuter, .WeekdayName {
@include make-col($size: 1, $columns: 3);
}
}
@include media-breakpoint-up(md) {
.DayOuter, .WeekdayName {
@include make-col($size: 1, $columns: 4);
}
}
@include media-breakpoint-up(lg) {
.DayOuter, .WeekdayName {
@include make-col($size: 1, $columns: 5);
}
}
@include media-breakpoint-up(xl) {
.Month-weekdayHead {
display: block;
@include make-row($gutter: 0);
position: sticky;
top: 0;
background: white;
margin-top: -$spacer * 7;
padding-top: $spacer * 7;
}
.DayOuter, .WeekdayName, .DayFiller {
@include make-col($size: 1, $columns: 7);
}
.Day-weekday {
display: none;
}
.DayFiller {
display: block;
}
}
.Month {
margin-left: -$calendar-gutter;
margin-right: -$calendar-gutter;
}
.Day, .DayFiller {
padding: 0.5em;
border-radius: $border-radius;
}
.Day {
flex: 1;
border: 1px solid black;
}
.DayFiller {
border: 1px solid transparent;
}
.WeekdayName {
text-align: center;
font-weight: 600;
font-size: 1.2em;
}
.Day-name {
font-size: 2em;
font-weight: 600;
}
.Day-weekday {
font-weight: 600;
}
.Day-today {
background: $gray-300;
}
.Calendar-month-name.input-group {
justify-content: center;
font-size: 0.75em;
}
.Calendar-month-input {
max-width: 12.5em;
}
.Calendar-year-input {
max-width: 6em;
}
.Calendar-today-button {
max-width: 5em;
}

213
jcal/src/Calendar.tsx Normal file
View file

@ -0,0 +1,213 @@
import React from 'react';
import './Calendar.scss';
import {formatJG, jdnGregorian, JulianDay, JulianMonth, monthName, weekdayNames} from '@common/gregorian';
import {jdnLongCount} from '@common/longCount';
import {jdnJulian, julianJDN, julianMonthDays} from '@common/julian';
import {frDateFormat, frEndJD, frStartJD, jdnFrench} from '@common/french';
type MonthProps = {
year: number;
month: JulianMonth;
};
type DateProps = MonthProps & {
day: JulianDay;
};
function WeekdayName({name}: { name: string }): JSX.Element {
return <div className="WeekdayName">{name}</div>;
}
function DayDetail({jdn}: { jdn: number }): JSX.Element {
const lc = jdnLongCount(jdn);
return <div className="DayDetail">
<div className="DayDetail-jdn"><abbr title="Julian day number">JD</abbr> {jdn}</div>
<div className="DayDetail-gregorian">
<abbr title="Gregorian date">G.</abbr>{' '}
{formatJG(jdnGregorian(jdn))}
</div>
{lc && <div className="DayDetail-lc">
<abbr title="Mesoamerican long count date">LC</abbr>{' '}
{lc.join('.\u200b')}
</div>}
{jdn >= frStartJD && jdn <= frEndJD && <div className="DayDetail-fr">
<abbr title="French Republican Calendar">FR</abbr>{' '}
{frDateFormat(jdnFrench(jdn))}
</div>}
</div>;
}
function Day({year, month, day, todayJDN}: DateProps & { todayJDN: number }): JSX.Element {
const jdn = julianJDN(year, month, day);
return <div className={`Day NormalDay ${jdn === todayJDN ? 'Day-today' : ''}`}>
<div className="Day-name">{day}</div>
<div className="Day-weekday">{weekdayNames[(day - 1) % 7]}</div>
<DayDetail jdn={jdn}/>
</div>;
}
function Month({year, month, todayJDN}: MonthProps & { todayJDN: number }): JSX.Element {
const decadeHeads = weekdayNames.map((name, i) => <WeekdayName key={i} name={name}/>);
const firstJDN = julianJDN(year, month, 1);
const firstWeekday = (firstJDN + 1) % 7;
const daysTotal = julianMonthDays(year, month);
return <div className="Month">
<div className="Month-weekdayHead">{decadeHeads}</div>
<div className="Month-days">{
Array.from(Array(6).keys()).flatMap(i => {
if (i * 7 - firstWeekday + 1 > daysTotal)
return [];
return Array.from(Array(7).keys()).map(j => {
const day = i * 7 + j - firstWeekday + 1 as JulianDay;
if (day < 1 || day > daysTotal)
return <div key={j} className="DayFiller"/>;
return <div key={j} className="DayOuter">
<Day year={year} month={month} day={day} todayJDN={todayJDN}/>
</div>;
});
})
}</div>
</div>;
}
export type CalendarProps = MonthProps & {
todayJDN: number;
onSwitch?: (year: number, month: JulianMonth) => void,
};
type CalendarState = {
selecting: boolean,
yearStr: string,
};
export class Calendar extends React.Component<CalendarProps, CalendarState> {
selection: React.RefObject<HTMLDivElement>;
constructor(props: CalendarProps) {
super(props);
this.state = {
selecting: false,
yearStr: this.props.year.toString(),
};
this.selection = React.createRef();
}
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
private goToNormalized(year: number, month: number) {
if (month < 1) {
--year;
month += 12;
}
if (month > 12) {
++year;
month -= 12;
}
this.props.onSwitch && this.props.onSwitch(year, month as JulianMonth);
}
prevYear = () => {
this.goToNormalized(this.props.year - 1, this.props.month);
};
prevMonth = () => {
this.goToNormalized(this.props.year, this.props.month - 1);
};
nextYear = () => {
this.goToNormalized(this.props.year + 1, this.props.month);
};
nextMonth = () => {
this.goToNormalized(this.props.year, this.props.month + 1);
};
startSelection = () => {
this.setState({selecting: true});
};
handleClickOutside = (event: any) => {
if (this.state.selecting && this.selection.current && !this.selection.current.contains(event.target))
this.setState({selecting: false});
};
handleKeyUp = (event: any) => {
if (event.key === 'Escape')
this.setState({selecting: false});
};
monthChange = (event: any) => {
this.goToNormalized(this.props.year, +event.target.value as JulianMonth);
};
yearChange = (event: any) => {
if (/^-?\d+/.test(event.target.value)) {
this.goToNormalized(+event.target.value, this.props.month);
}
this.setState({yearStr: event.target.value});
};
goToToday = () => {
const [year, month] = jdnJulian(this.props.todayJDN);
this.goToNormalized(year, month);
this.setState({selecting: false});
};
componentDidUpdate(prevProps: CalendarProps) {
if (prevProps.year !== this.props.year) {
const yearStr = this.props.year.toString();
if (this.state.yearStr !== yearStr) {
this.setState({
yearStr: yearStr,
});
}
}
}
render(): JSX.Element {
return <div className="Calendar">
<div className="Calendar-head">
<div className="Calendar-prev">
<button type="button" className="btn btn-secondary" title="Previous year" onClick={this.prevYear}>«
</button>
<button type="button" className="btn btn-secondary" title="Previous month"
onClick={this.prevMonth}>
</button>
</div>
{!this.state.selecting && <div className="Calendar-month-name" onClick={this.startSelection}>
{monthName(this.props.month)} {this.props.year}
</div>}
{this.state.selecting && <div className="Calendar-month-name input-group" ref={this.selection}
onKeyUp={this.handleKeyUp}>
<select className="Calendar-month-input form-control" onChange={this.monthChange}
value={this.props.month}>{
Array.from(Array(12).keys()).map(i => {
const month = i + 1 as JulianMonth;
return <option key={i} value={month}>{monthName(month)}</option>;
})
}</select>
<input type="number" className="Calendar-year-input form-control" value={this.state.yearStr}
onChange={this.yearChange}/>
<button type="button" className="form-control btn btn-primary Calendar-today-button"
onClick={this.goToToday}>Today
</button>
</div>}
<div className="Calendar-next">
<button type="button" className="btn btn-secondary" title="Next month" onClick={this.nextMonth}>
</button>
<button type="button" className="btn btn-secondary" title="Next year" onClick={this.nextYear}>»
</button>
</div>
</div>
<Month year={this.props.year} month={this.props.month} todayJDN={this.props.todayJDN}/>
</div>;
}
}

2
jcal/src/consts.scss Normal file
View file

@ -0,0 +1,2 @@
$calendar-width: 1600px;
$calendar-gutter: 0.5rem;

99
jcal/src/index.scss Normal file
View file

@ -0,0 +1,99 @@
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/reboot';
@import 'bootstrap/scss/navbar';
@import 'bootstrap/scss/transitions';
@import 'bootstrap/scss/type';
@import 'bootstrap/scss/card';
@import 'bootstrap/scss/forms';
@import 'bootstrap/scss/root';
@import './consts';
body {
padding-top: $spacer * 4;
position: relative;
}
#root {
max-width: $calendar-width;
margin: 0 auto;
}
nav.navbar {
background: $light;
position: fixed;
top: 0;
z-index: 100;
left: 0;
right: 0;
.container {
max-width: $calendar-width;
width: 100%;
margin: 0 auto;
padding: 0 1em;
}
}
@include media-breakpoint-down(sm) {
.hide-small {
display: none;
}
}
.navigate {
max-width: $calendar-width;
margin-top: $spacer;
@include make-container();
.go-year {
max-width: 7em;
}
.go-month, .go-day {
max-width: 5em;
}
.go-button {
max-width: 3em;
}
}
.main {
max-width: $calendar-width;
margin-top: $spacer;
@include make-container();
.card {
margin-bottom: $spacer;
}
}
html {
scroll-padding-top: $spacer * 4;
}
@supports not (scroll-padding-top: 80px) {
h1, h2, h3, h4, h5, h6, .footnotes li {
margin-top: -$spacer * 4;
padding-top: $spacer * 4;
}
}
a {
text-decoration: none;
}
.footer {
background: $light;
min-height: $spacer * 4;
padding: ($spacer * 4 - $font-size-base) / 2 0;
.container {
width: 100%;
max-width: $calendar-width;
margin: 0 auto;
padding: 0 1em;
}
}

18
jcal/src/index.tsx Normal file
View file

@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/js/dist/collapse';
import './index.scss';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
jcal/src/react-app-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View file

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
jcal/src/setupTests.ts Normal file
View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

27
jcal/tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

8
jcal/tsconfig.paths.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@common/*": ["../common/src/*"]
}
}
}

17480
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

8
package.json Normal file
View file

@ -0,0 +1,8 @@
{
"name": "qcal",
"workspaces": [
"common",
"frcal",
"jcal"
]
}