mirror of
https://github.com/quantum5/qcal.git
synced 2025-07-26 19:34:10 -04:00
mcal: fork from frcal
This commit is contained in:
parent
6bbd0d0ad9
commit
d344b44433
9
mcal/config-overrides.js
Normal file
9
mcal/config-overrides.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const {aliasWebpack, aliasJest} = require('react-app-alias-ex');
|
||||||
|
|
||||||
|
const options = {};
|
||||||
|
module.exports = aliasWebpack(options);
|
||||||
|
module.exports.jest = function (config) {
|
||||||
|
const result = aliasJest(options)(config);
|
||||||
|
result.moduleDirectories.unshift(require('path').resolve(__dirname, '../node_modules'));
|
||||||
|
return result;
|
||||||
|
};
|
163
mcal/index.html
Normal file
163
mcal/index.html
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<!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="%PUBLIC_URL%"/>
|
||||||
|
<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>French Republican Calendar (a.k.a. French Revolutionary Calendar)</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-md navbar-light">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="#">French Republican 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://jcal.qt.ax">Julian Calendar</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://gcal.qt.ax">Gregorian 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/French_Republican_calendar">French Republican
|
||||||
|
calendar</a> was a calendar created and implemented during the French Revolution.</p>
|
||||||
|
<p>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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">How does it work?</h4>
|
||||||
|
<p class="lead">A year consists of 12 months of 30 days each, divided into three <em>décades</em> of 10 days
|
||||||
|
each, followed by 5 complementary days (6 in leap years).</p>
|
||||||
|
<p>The year starts on the day of the autumnal equinox at the Paris Observatory (longitude 2°20′14.03″ E). A
|
||||||
|
leap year follow directly from this definition: a year is a leap year when the next autumnal equinox
|
||||||
|
happens 366 days later instead of the normal 365. By this definition, the year will <b>never</b> drift
|
||||||
|
with respect to the seasons.</p>
|
||||||
|
<p>The 12 months are: <em>Vendémiaire</em>, <em>Brumaire</em>, <em>Frimaire</em>, <em>Nivôse</em>, <em>Pluviôse</em>,
|
||||||
|
<em>Ventôse</em>, <em>Germinal</em>, <em>Floréal</em>, <em>Prairial</em>, <em>Messidor</em>, <em>Thermidor</em>,
|
||||||
|
<em>Fructidor.</em> Every three months represent a season, and the endings of the names reflect this
|
||||||
|
fact.</p>
|
||||||
|
<p>The complementary days are: <em>la Fête de la Vertu</em>, <em>la Fête du Génie</em>, <em>la Fête du
|
||||||
|
Travail</em>, <em>la Fête de l'Opinion</em>, <em>la Fête des Récompenses,</em> and <em>la Fête de la
|
||||||
|
Révolution</em> (leap years only).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What's so special about this version?</h4>
|
||||||
|
<p class="lead">Most versions of the calendar floating around doesn't use the original definition above.</p>
|
||||||
|
<p>Most versions uses the so-called <em>Romme</em> method for leap years, using the same leap year rules as
|
||||||
|
the Gregorian calendar, i.e. every year divisible by four, except century years not divisible by 400.
|
||||||
|
This method might make sense, except years 3, 7, and 11 were leap years under the original rules and
|
||||||
|
were observed as such in real life, but the <em>Romme</em> method instead makes years 4, 8, 12 leap
|
||||||
|
years instead.</p>
|
||||||
|
<p>This version uses the original rules. The <a href="https://ssd.jpl.nasa.gov/planets/eph_export.html">JPL's
|
||||||
|
DE440 and DE441 ephemerides</a> were used to calculate the exact timings of the autumnal equinoxes
|
||||||
|
between the Gregorian years 13201 BCE and 17191 CE (corresponding to the French Republican years -14991
|
||||||
|
to 15399). The times were then converted to UT1+00:09:21, the exact local time at the Paris Observatory.
|
||||||
|
UT1 was chosen to keep track of the Earth's rotation without having to worry about the issues posed by
|
||||||
|
leap seconds in UTC. Note that due to the uncertainty over
|
||||||
|
<a href="https://en.wikipedia.org/wiki/%CE%94T_(timekeeping)">ΔT</a> — the difference between UT1 and
|
||||||
|
Terrestrial Time (TT) used in the ephemerides — it is theoretically possible for there to be
|
||||||
|
inaccuracies when the equinox occurs very close to midnight.</p>
|
||||||
|
<p>For more details about how I calculated this calendar, please see
|
||||||
|
<a href="https://quantum5.ca/2022/03/09/art-of-time-keeping-part-4-french-republican-calendar/">my blog
|
||||||
|
post on the topic</a>. This is the fourth part of a series on time-keeping, and you are highly
|
||||||
|
encouraged to read the
|
||||||
|
<a href="https://quantum5.ca/2022/02/16/art-of-time-keeping-part-1-years-dates/">first</a>
|
||||||
|
<a href="https://quantum5.ca/2022/02/23/art-of-time-keeping-part-2-time/">three</a>
|
||||||
|
<a href="https://quantum5.ca/2022/03/02/art-of-time-keeping-part-3-astronomy-equinoxes/">parts</a>
|
||||||
|
for a more complete understanding.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What are those names above the Gregorian date?</h4>
|
||||||
|
<p>Those are the names of the days in the
|
||||||
|
<a href="https://en.wikipedia.org/wiki/French_Republican_calendar#Rural_calendar">rural version of the
|
||||||
|
calendar</a>. This was intended to replace the Catholic Church's calendar of saints, as the French
|
||||||
|
Revolution wanted to reduce the influence of the church. Every day of the year has a unique name
|
||||||
|
associated with the rural economy and these names are supposed to correspond with the season.</p>
|
||||||
|
<p>Every <em>quintidi</em> is named after an animal, every <em>décadi</em> is named after an agricultural
|
||||||
|
tool, and the remaining days are named after various plants or produce. The only exception is the winter
|
||||||
|
month of Nivôse, which has the remaining days named after minerals.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What are those numbers below the Gregorian 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 “Mayan calendar.” This calendar is not
|
||||||
|
available for dates before August 11, 3114 BCE (25 Thermidor -4905).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What is decimal time?</h4>
|
||||||
|
<p class="lead">Decimal time is a time system used during the French Revolution that divided the day into 10
|
||||||
|
hours, each with 100 minutes, which contained 100 seconds each.</p>
|
||||||
|
<p>The result is 100,000 seconds in one day, compared to the 86,400 seconds with the normal 24-hour
|
||||||
|
system. This makes it very easy to denote time as a decimal fraction of a day. For example, decimal time
|
||||||
|
5:67:72 (around 13:37:31) on January 1, 2000 can be represented as <code>2000-01-01.56772</code>.</p>
|
||||||
|
<p>Also note that each decimal hour is 2.4 normal hours, each decimal minute is 1.44 normal minutes, and
|
||||||
|
each decimal second is 0.864 normal seconds.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p class="text-muted">Copyright © 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/qcal">GitHub</a>.<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
(function(r,e,p,u,b,l,i,c){r['GoogleAnalyticsObject']=b;r[b]=r[b]||function(){
|
||||||
|
(r[b].q=r[b].q||[]).push(arguments)},r[b].l=1*new Date();l=e.createElement(p),
|
||||||
|
i=e.getElementsByTagName(p)[0];l.async=1;l.src=u;i.parentNode.insertBefore(l,i)
|
||||||
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-102581070-4', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
mcal/manifest.json
Normal file
25
mcal/manifest.json
Normal 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"
|
||||||
|
}
|
51
mcal/package.json
Normal file
51
mcal/package.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "mcal",
|
||||||
|
"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
mcal/public/favicon.ico
Normal file
BIN
mcal/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
163
mcal/public/index.html
Normal file
163
mcal/public/index.html
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<!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="%PUBLIC_URL%"/>
|
||||||
|
<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>French Republican Calendar (a.k.a. French Revolutionary Calendar)</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-md navbar-light">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="#">French Republican 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://jcal.qt.ax">Julian Calendar</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://gcal.qt.ax">Gregorian 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/French_Republican_calendar">French Republican
|
||||||
|
calendar</a> was a calendar created and implemented during the French Revolution.</p>
|
||||||
|
<p>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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">How does it work?</h4>
|
||||||
|
<p class="lead">A year consists of 12 months of 30 days each, divided into three <em>décades</em> of 10 days
|
||||||
|
each, followed by 5 complementary days (6 in leap years).</p>
|
||||||
|
<p>The year starts on the day of the autumnal equinox at the Paris Observatory (longitude 2°20′14.03″ E). A
|
||||||
|
leap year follow directly from this definition: a year is a leap year when the next autumnal equinox
|
||||||
|
happens 366 days later instead of the normal 365. By this definition, the year will <b>never</b> drift
|
||||||
|
with respect to the seasons.</p>
|
||||||
|
<p>The 12 months are: <em>Vendémiaire</em>, <em>Brumaire</em>, <em>Frimaire</em>, <em>Nivôse</em>, <em>Pluviôse</em>,
|
||||||
|
<em>Ventôse</em>, <em>Germinal</em>, <em>Floréal</em>, <em>Prairial</em>, <em>Messidor</em>, <em>Thermidor</em>,
|
||||||
|
<em>Fructidor.</em> Every three months represent a season, and the endings of the names reflect this
|
||||||
|
fact.</p>
|
||||||
|
<p>The complementary days are: <em>la Fête de la Vertu</em>, <em>la Fête du Génie</em>, <em>la Fête du
|
||||||
|
Travail</em>, <em>la Fête de l'Opinion</em>, <em>la Fête des Récompenses,</em> and <em>la Fête de la
|
||||||
|
Révolution</em> (leap years only).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What's so special about this version?</h4>
|
||||||
|
<p class="lead">Most versions of the calendar floating around doesn't use the original definition above.</p>
|
||||||
|
<p>Most versions uses the so-called <em>Romme</em> method for leap years, using the same leap year rules as
|
||||||
|
the Gregorian calendar, i.e. every year divisible by four, except century years not divisible by 400.
|
||||||
|
This method might make sense, except years 3, 7, and 11 were leap years under the original rules and
|
||||||
|
were observed as such in real life, but the <em>Romme</em> method instead makes years 4, 8, 12 leap
|
||||||
|
years instead.</p>
|
||||||
|
<p>This version uses the original rules. The <a href="https://ssd.jpl.nasa.gov/planets/eph_export.html">JPL's
|
||||||
|
DE440 and DE441 ephemerides</a> were used to calculate the exact timings of the autumnal equinoxes
|
||||||
|
between the Gregorian years 13201 BCE and 17191 CE (corresponding to the French Republican years -14991
|
||||||
|
to 15399). The times were then converted to UT1+00:09:21, the exact local time at the Paris Observatory.
|
||||||
|
UT1 was chosen to keep track of the Earth's rotation without having to worry about the issues posed by
|
||||||
|
leap seconds in UTC. Note that due to the uncertainty over
|
||||||
|
<a href="https://en.wikipedia.org/wiki/%CE%94T_(timekeeping)">ΔT</a> — the difference between UT1 and
|
||||||
|
Terrestrial Time (TT) used in the ephemerides — it is theoretically possible for there to be
|
||||||
|
inaccuracies when the equinox occurs very close to midnight.</p>
|
||||||
|
<p>For more details about how I calculated this calendar, please see
|
||||||
|
<a href="https://quantum5.ca/2022/03/09/art-of-time-keeping-part-4-french-republican-calendar/">my blog
|
||||||
|
post on the topic</a>. This is the fourth part of a series on time-keeping, and you are highly
|
||||||
|
encouraged to read the
|
||||||
|
<a href="https://quantum5.ca/2022/02/16/art-of-time-keeping-part-1-years-dates/">first</a>
|
||||||
|
<a href="https://quantum5.ca/2022/02/23/art-of-time-keeping-part-2-time/">three</a>
|
||||||
|
<a href="https://quantum5.ca/2022/03/02/art-of-time-keeping-part-3-astronomy-equinoxes/">parts</a>
|
||||||
|
for a more complete understanding.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What are those names above the Gregorian date?</h4>
|
||||||
|
<p>Those are the names of the days in the
|
||||||
|
<a href="https://en.wikipedia.org/wiki/French_Republican_calendar#Rural_calendar">rural version of the
|
||||||
|
calendar</a>. This was intended to replace the Catholic Church's calendar of saints, as the French
|
||||||
|
Revolution wanted to reduce the influence of the church. Every day of the year has a unique name
|
||||||
|
associated with the rural economy and these names are supposed to correspond with the season.</p>
|
||||||
|
<p>Every <em>quintidi</em> is named after an animal, every <em>décadi</em> is named after an agricultural
|
||||||
|
tool, and the remaining days are named after various plants or produce. The only exception is the winter
|
||||||
|
month of Nivôse, which has the remaining days named after minerals.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What are those numbers below the Gregorian 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 “Mayan calendar.” This calendar is not
|
||||||
|
available for dates before August 11, 3114 BCE (25 Thermidor -4905).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">What is decimal time?</h4>
|
||||||
|
<p class="lead">Decimal time is a time system used during the French Revolution that divided the day into 10
|
||||||
|
hours, each with 100 minutes, which contained 100 seconds each.</p>
|
||||||
|
<p>The result is 100,000 seconds in one day, compared to the 86,400 seconds with the normal 24-hour
|
||||||
|
system. This makes it very easy to denote time as a decimal fraction of a day. For example, decimal time
|
||||||
|
5:67:72 (around 13:37:31) on January 1, 2000 can be represented as <code>2000-01-01.56772</code>.</p>
|
||||||
|
<p>Also note that each decimal hour is 2.4 normal hours, each decimal minute is 1.44 normal minutes, and
|
||||||
|
each decimal second is 0.864 normal seconds.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p class="text-muted">Copyright © 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/qcal">GitHub</a>.<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
(function(r,e,p,u,b,l,i,c){r['GoogleAnalyticsObject']=b;r[b]=r[b]||function(){
|
||||||
|
(r[b].q=r[b].q||[]).push(arguments)},r[b].l=1*new Date();l=e.createElement(p),
|
||||||
|
i=e.getElementsByTagName(p)[0];l.async=1;l.src=u;i.parentNode.insertBefore(l,i)
|
||||||
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-102581070-4', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
mcal/public/logo.svg
Normal file
1
mcal/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.1 KiB |
BIN
mcal/public/logo192.png
Normal file
BIN
mcal/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
mcal/public/logo512.png
Normal file
BIN
mcal/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
25
mcal/public/manifest.json
Normal file
25
mcal/public/manifest.json
Normal 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
mcal/public/robots.txt
Normal file
3
mcal/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
3
mcal/robots.txt
Normal file
3
mcal/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
45
mcal/src/App.tsx
Normal file
45
mcal/src/App.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Calendar} from './Calendar';
|
||||||
|
import {FrenchMonth, frEndJD, frStartJD, frSupportedYear, jdnFrench} from '@common/french';
|
||||||
|
import {JulianMonth} from '@common/gregorian';
|
||||||
|
import {GregorianJumper} from '@common/dateJump';
|
||||||
|
import MonthBasedApp from '@common/ui/MonthBasedApp';
|
||||||
|
import Export from './Export';
|
||||||
|
|
||||||
|
export default class App extends MonthBasedApp<number, FrenchMonth> {
|
||||||
|
override parseYearMonth(year: string, month: string) {
|
||||||
|
if (!frSupportedYear(+year) || +month < 1 || +month > 13)
|
||||||
|
return null;
|
||||||
|
return {year: +year, month: +month as JulianMonth};
|
||||||
|
}
|
||||||
|
|
||||||
|
override defaultSelector(todayJDN: number) {
|
||||||
|
const {year, month} = jdnFrench(todayJDN);
|
||||||
|
return {year, month};
|
||||||
|
}
|
||||||
|
|
||||||
|
goToJDN = (jdn: number) => {
|
||||||
|
const {year, month} = jdnFrench(Math.min(Math.max(frStartJD, jdn), frEndJD));
|
||||||
|
this.setState({selector: {year, month}});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {selector: {year, month}, todayJDN} = this.state;
|
||||||
|
return <>
|
||||||
|
<Calendar
|
||||||
|
year={year} month={month} todayJDN={todayJDN}
|
||||||
|
onSwitch={(year, month) => this.setState({selector: {year, month}})}/>
|
||||||
|
|
||||||
|
<div className="navigate">
|
||||||
|
<h4>Go to a date</h4>
|
||||||
|
<GregorianJumper minJDN={frStartJD} maxJDN={frEndJD} initialJDN={todayJDN}
|
||||||
|
onJump={this.goToJDN}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="download">
|
||||||
|
<h4>Export calendar</h4>
|
||||||
|
<Export minJDN={frStartJD} maxJDN={frEndJD} initialJDN={todayJDN}/>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
71
mcal/src/Calendar.scss
Normal file
71
mcal/src/Calendar.scss
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
@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 '@common/ui/consts.scss';
|
||||||
|
@import '@common/ui/MonthBasedCalendar.scss';
|
||||||
|
|
||||||
|
@include media-breakpoint-up(xs) {
|
||||||
|
.DayOuter.NormalDay {
|
||||||
|
@include make-col($size: 1, $columns: 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DayOuter.ComplementaryDay {
|
||||||
|
@include make-col($size: 1, $columns: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
.DayOuter.NormalDay {
|
||||||
|
@include make-col($size: 1, $columns: 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
.DayOuter.NormalDay {
|
||||||
|
@include make-col($size: 1, $columns: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
.DayOuter.NormalDay {
|
||||||
|
@include make-col($size: 1, $columns: 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DayOuter.ComplementaryDay {
|
||||||
|
@include make-col($size: 1, $columns: 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(xl) {
|
||||||
|
.DayOuter.NormalDay {
|
||||||
|
@include make-col($size: 1, $columns: 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(xxl) {
|
||||||
|
.Month-weekdayHead {
|
||||||
|
display: block;
|
||||||
|
@include make-row($gutter: 0);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: white;
|
||||||
|
margin-top: -$spacer * 7.5;
|
||||||
|
padding-top: $spacer * 7.5;
|
||||||
|
z-index: 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Month-days {
|
||||||
|
margin-top: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DayOuter.NormalDay, .WeekdayName {
|
||||||
|
@include make-col($size: 1, $columns: 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day-weekday {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
167
mcal/src/Calendar.tsx
Normal file
167
mcal/src/Calendar.tsx
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './Calendar.scss';
|
||||||
|
import {
|
||||||
|
dateName,
|
||||||
|
dateRuralName,
|
||||||
|
decadeNames,
|
||||||
|
FrenchDay,
|
||||||
|
FrenchMonth,
|
||||||
|
frEndYear,
|
||||||
|
frIsLeap,
|
||||||
|
frJDN,
|
||||||
|
frStartYear,
|
||||||
|
jdnFrench,
|
||||||
|
monthName,
|
||||||
|
} from '@common/french';
|
||||||
|
import {jdnDate} from '@common/gregorian';
|
||||||
|
import {jdnLongCount} from '@common/longCount';
|
||||||
|
import {useMobileTooltipProps} from '@common/ui/MobileTooltip';
|
||||||
|
import {MonthBasedCalendar} from '@common/ui/MonthBasedCalendar';
|
||||||
|
|
||||||
|
type FrenchYear = number;
|
||||||
|
|
||||||
|
type MonthProps = {
|
||||||
|
year: FrenchYear;
|
||||||
|
month: FrenchMonth;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DateProps = MonthProps & {
|
||||||
|
day: FrenchDay;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DecadeName({name}: { name: string }): JSX.Element {
|
||||||
|
return <div className="WeekdayName">{name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DayDetail({jdn}: { jdn: number }): JSX.Element {
|
||||||
|
return <div className="DayDetail">
|
||||||
|
<div className="DayDetail-gregorian">{jdnDate(jdn).toDateString()}</div>
|
||||||
|
<div className="DayDetail-lc">{jdnLongCount(jdn)?.join('.')}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NormalDay({year, month, day, todayJDN}: DateProps & { todayJDN: number }): JSX.Element {
|
||||||
|
const jdn = frJDN(year, month, day);
|
||||||
|
const rural = dateRuralName(month, day)!;
|
||||||
|
const mobile = useMobileTooltipProps();
|
||||||
|
return <div className={`Day ${jdn === todayJDN ? 'Day-today' : ''}`}>
|
||||||
|
<div className="Day-name">{day}</div>
|
||||||
|
<div className="Day-weekday">{decadeNames[(day - 1) % 10]}</div>
|
||||||
|
<div className="Day-rural"><abbr title={rural.title} {...mobile}>{rural.name}</abbr></div>
|
||||||
|
<DayDetail jdn={jdn}/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NormalMonth({year, month, todayJDN}: MonthProps & { todayJDN: number }): JSX.Element {
|
||||||
|
const decadeHeads = decadeNames.map((name, i) => <DecadeName key={i} name={name}/>);
|
||||||
|
return <div className="Month">
|
||||||
|
<div className="Month-weekdayHead">{decadeHeads}</div>
|
||||||
|
<div className="Month-days">{
|
||||||
|
Array.from(Array(30).keys()).map(i => <div key={i} className="DayOuter NormalDay">
|
||||||
|
<NormalDay year={year} month={month} day={i + 1 as FrenchDay} todayJDN={todayJDN}/>
|
||||||
|
</div>)
|
||||||
|
}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComplementaryDay({year, month, day, todayJDN}: DateProps & { todayJDN: number }): JSX.Element {
|
||||||
|
const jdn = frJDN(year, month, day);
|
||||||
|
return <div className={`Day ${jdn === todayJDN ? 'Day-today' : ''}`}>
|
||||||
|
<div className="Day-name">{dateName(month, day)}</div>
|
||||||
|
<DayDetail jdn={jdn}/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComplementaryDays({year, todayJDN}: { year: FrenchYear, todayJDN: number }): JSX.Element {
|
||||||
|
const leap = frIsLeap(year);
|
||||||
|
return <div className="Month">
|
||||||
|
<div className="Month-days">{
|
||||||
|
Array.from(Array(leap ? 6 : 5).keys()).map(i => <div key={i} className="DayOuter ComplementaryDay">
|
||||||
|
<ComplementaryDay year={year} month={13} day={i + 1 as FrenchDay} todayJDN={todayJDN}/>
|
||||||
|
</div>)
|
||||||
|
}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Calendar extends MonthBasedCalendar<FrenchYear, FrenchMonth> {
|
||||||
|
override parseYear(year: string): FrenchYear {
|
||||||
|
return +year;
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseMonth(month: string): FrenchMonth {
|
||||||
|
return +month as FrenchMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
override yearToString(year: FrenchYear): string {
|
||||||
|
return year.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
override monthToString(month: FrenchMonth): string {
|
||||||
|
return month.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private goToNormalized(year: number, month: number) {
|
||||||
|
while (month < 1) {
|
||||||
|
--year;
|
||||||
|
month += 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (month > 13) {
|
||||||
|
++year;
|
||||||
|
month -= 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year < frStartYear) {
|
||||||
|
year = frStartYear;
|
||||||
|
month = 1;
|
||||||
|
} else if (year > frEndYear) {
|
||||||
|
year = frEndYear;
|
||||||
|
month = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goTo(year, month as FrenchMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
override prevYear = () => {
|
||||||
|
this.goToNormalized(this.props.year - 1, this.props.month);
|
||||||
|
};
|
||||||
|
|
||||||
|
override prevMonth = () => {
|
||||||
|
this.goToNormalized(this.props.year, this.props.month - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
override nextYear = () => {
|
||||||
|
this.goToNormalized(this.props.year + 1, this.props.month);
|
||||||
|
};
|
||||||
|
|
||||||
|
override nextMonth = () => {
|
||||||
|
this.goToNormalized(this.props.year, this.props.month + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
override isValidYear(year: string): boolean {
|
||||||
|
return /^-?\d+/.test(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
override jdnLookup(jdn: number): { year: FrenchYear; month: FrenchMonth } {
|
||||||
|
return jdnFrench(jdn);
|
||||||
|
}
|
||||||
|
|
||||||
|
override monthName(year: FrenchYear, month: FrenchMonth): string {
|
||||||
|
return month === 13 ? year.toString() : `${monthName(month)} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderMonthOptions(): JSX.Element[] {
|
||||||
|
return Array.from(Array(13).keys()).map(i => {
|
||||||
|
const month = i + 1 as FrenchMonth;
|
||||||
|
return <option key={i} value={month}>{monthName(month)}</option>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderBody(): JSX.Element {
|
||||||
|
if (this.props.month < 13) {
|
||||||
|
return <NormalMonth year={this.props.year} month={this.props.month} todayJDN={this.props.todayJDN}/>;
|
||||||
|
} else {
|
||||||
|
return <ComplementaryDays year={this.props.year} todayJDN={this.props.todayJDN}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
mcal/src/Export.tsx
Normal file
92
mcal/src/Export.tsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import {dateName, jdnFrench} from '@common/french';
|
||||||
|
import {jdnGregorian} from '@common/gregorian';
|
||||||
|
import React from 'react';
|
||||||
|
import GregorianSelector from '@common/dateJump/GregorianSelector';
|
||||||
|
import {BaseDateProps} from '@common/dateJump/base';
|
||||||
|
|
||||||
|
function zeroPad(item: unknown, width: number) {
|
||||||
|
const n = item + '';
|
||||||
|
return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function* iCalStream(startJDN: number, endJDN: number): Generator<string> {
|
||||||
|
yield `BEGIN:VCALENDAR\r
|
||||||
|
VERSION:2.0\r
|
||||||
|
PRODID:-//hacksw/handcal//NONSGML v1.0//EN\r
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (let jdn = startJDN; jdn <= endJDN; ++jdn) {
|
||||||
|
const [gy, gm, gd] = jdnGregorian(jdn);
|
||||||
|
const {year: fy, month: fm, day: fd} = jdnFrench(jdn);
|
||||||
|
yield `BEGIN:VEVENT\r
|
||||||
|
UID:jdn-${jdn}@frcal.qt.ax\r
|
||||||
|
DTSTAMP:${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '')}\r
|
||||||
|
DTSTART;VALUE=DATE:${zeroPad(gy, 4)}${zeroPad(gm, 2)}${zeroPad(gd, 2)}\r
|
||||||
|
SUMMARY:${dateName(fm, fd)} ${fy}\r
|
||||||
|
TRANSP:TRANSPARENT\r
|
||||||
|
END:VEVENT\r
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield 'END:VCALENDAR\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function iCalBlob(startJDN: number, endJDN: number): Blob {
|
||||||
|
return new Blob(iCalStream(startJDN, endJDN) as any, {type: 'text/calendar'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAs(url: string, name: string) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = name;
|
||||||
|
a.style.display = 'none';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = BaseDateProps;
|
||||||
|
|
||||||
|
export default function Export(props: Props): JSX.Element {
|
||||||
|
const [rangeStart, setRangeStart] = React.useState<number | undefined>();
|
||||||
|
const [rangeEnd, setRangeEnd] = React.useState<number | undefined>();
|
||||||
|
|
||||||
|
function doExport(event: React.FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (rangeStart === undefined || rangeEnd === undefined) {
|
||||||
|
alert('Date out of range!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = rangeEnd - rangeStart;
|
||||||
|
if (days > 36500 && !window.confirm(
|
||||||
|
`You are exporting ${days} days of calendar data. This may crash your computer. Do you want to continue?`,
|
||||||
|
))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const blob = iCalBlob(rangeStart, rangeEnd);
|
||||||
|
const [sy, sm, sd] = jdnGregorian(rangeStart);
|
||||||
|
const [ey, em, ed] = jdnGregorian(rangeEnd);
|
||||||
|
const startDate = `${zeroPad(sy, 4)}-${zeroPad(sm, 2)}-${zeroPad(sd, 2)}`;
|
||||||
|
const endDate = `${zeroPad(ey, 4)}-${zeroPad(em, 2)}-${zeroPad(ed, 2)}`;
|
||||||
|
downloadAs(URL.createObjectURL(blob), `frcal-${startDate}-${endDate}.ics`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={doExport}>
|
||||||
|
<div className="input-group gregorian-select">
|
||||||
|
<span className="input-group-text">Start date</span>
|
||||||
|
<GregorianSelector onChange={setRangeStart} {...props}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group gregorian-select">
|
||||||
|
<span className="input-group-text">End date</span>
|
||||||
|
<GregorianSelector onChange={setRangeEnd} {...props} initialJDN={props.initialJDN + 365 * 5}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="form-control btn btn-primary"
|
||||||
|
title="Export calendar as .ics file (iCalendar format)">Export
|
||||||
|
</button>
|
||||||
|
</form>;
|
||||||
|
}
|
30
mcal/src/index.scss
Normal file
30
mcal/src/index.scss
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
@import '@common/ui/index.scss';
|
||||||
|
|
||||||
|
.download {
|
||||||
|
max-width: $calendar-width;
|
||||||
|
margin-top: $spacer;
|
||||||
|
@include make-container();
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
max-width: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
.download form {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
width: fit-content;
|
||||||
|
margin-right: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
mcal/src/index.tsx
Normal file
21
mcal/src/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import 'bootstrap/js/dist/collapse';
|
||||||
|
import './index.scss';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from '@common/ui/reportWebVitals';
|
||||||
|
import {MobileTooltipProvider} from '@common/ui/MobileTooltip';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<MobileTooltipProvider>
|
||||||
|
<App/>
|
||||||
|
</MobileTooltipProvider>
|
||||||
|
</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
mcal/src/react-app-env.d.ts
vendored
Normal file
1
mcal/src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
5
mcal/src/setupTests.ts
Normal file
5
mcal/src/setupTests.ts
Normal 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
mcal/tsconfig.json
Normal file
27
mcal/tsconfig.json
Normal 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
mcal/tsconfig.paths.json
Normal file
8
mcal/tsconfig.paths.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@common/*": ["../common/src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue