refacto: Remake the interface so it is more 'vintage' looking + Adding a full calendar page

This commit is contained in:
gauvainboiche
2026-10-25 12:01:00 +01:00
parent 2539b6c756
commit f6738972f1
45 changed files with 1433 additions and 361 deletions
+2
View File
@@ -4,3 +4,5 @@ npm-debug.log*
.gitignore
*.pdf
README.md
src/images
scripts
+16 -1
View File
@@ -1,3 +1,18 @@
# calendrier_republicain
Convertisseur date actuelle en date du calendrier républicain
Convertisseur date actuelle en date du calendrier républicain.
Deux vues : la date du jour (ou d'une date choisie) sur une stèle illustrée de la gravure du mois, et la page « Le calendrier complet » (`#/calendar`) qui reprend la disposition de l'estampe de 1794 : quatre saisons, douze mois, les 360 jours et leurs noms, plus les jours complémentaires.
## Lancer
```
node server.js # http://localhost:3000
docker compose up --build
```
## Ressources graphiques
- `src/images/` : gravures d'origine (scans). `public/assets/months/` en contient les versions recadrées et allégées, générées par `scripts/prepare-images.cjs` (`npm i --no-save sharp` au préalable ; sharp n'est pas une dépendance du projet).
- `public/assets/fonts/` : Old Standard TT (texte) et IM Fell English SC (titres), toutes deux sous licence SIL OFL (textes de licence dans le même dossier), servies localement.
- `public/data/day-names.json` : noms des 360 jours (Fabre d'Églantine), en français dans les deux langues.
+28 -2
View File
@@ -10,7 +10,8 @@
"aboutHistory": "History of the calendar",
"aboutLeapYears": "Sextile years",
"aboutComplementaryDays": "The complementary days",
"language": "Language"
"language": "Language",
"calendar": "Full calendar"
},
"home": {
"todayHeading": "Today is",
@@ -44,5 +45,30 @@
"text": "Dates computed with the perpetual arithmetic rule proposed by Gilbert Romme (19 Floréal, Year III / 8 May 1795).",
"sourceNote": "Historical content adapted from the \"French Republican calendar\" reference article."
},
"loading": "Loading…"
"loading": "Loading…",
"calendar": {
"title": "Calendar of the French Republic",
"subtitle": "The twelve months, the three décades and the days of the year",
"seasons": {
"autumn": "Autumn",
"winter": "Winter",
"spring": "Spring",
"summer": "Summer"
},
"decades": [
"1st décade",
"2nd décade",
"3rd décade"
],
"dayColumn": "Days",
"explanationTitle": "Explanation",
"explanation": [
"The Republican year begins at the autumnal equinox. It has twelve months of thirty days, split into three décades of ten days, plus five complementary days, or six in sextile years.",
"Each day is named after a plant, an animal, a mineral or a farming tool: the fifth day of each décade honours an animal, the tenth a tool, and in Nivôse the days honour minerals. The day names are given in French, as in the original.",
"Tap a month name to see its engraving and its origin. The day shown on the home page is highlighted."
],
"feastsTitle": "Complementary days",
"sextileOnly": "sextile years only",
"close": "Close"
}
}
+28 -2
View File
@@ -10,7 +10,8 @@
"aboutHistory": "Histoire du calendrier",
"aboutLeapYears": "Les années sextiles",
"aboutComplementaryDays": "Les jours complémentaires",
"language": "Langue"
"language": "Langue",
"calendar": "Le calendrier complet"
},
"home": {
"todayHeading": "Nous sommes aujourd'hui le",
@@ -44,5 +45,30 @@
"text": "Dates calculées selon la règle arithmétique perpétuelle proposée par Gilbert Romme (19 floréal an III / 8 mai 1795).",
"sourceNote": "Contenu historique inspiré de l'article « Calendrier républicain »."
},
"loading": "Chargement…"
"loading": "Chargement…",
"calendar": {
"title": "Calendrier de la République française",
"subtitle": "Les douze mois, les trois décades et les jours de l'année",
"seasons": {
"autumn": "Automne",
"winter": "Hiver",
"spring": "Printemps",
"summer": "Été"
},
"decades": [
"1re décade",
"2e décade",
"3e décade"
],
"dayColumn": "Jours",
"explanationTitle": "Explication",
"explanation": [
"L'année républicaine commence à l'équinoxe d'automne. Elle compte douze mois de trente jours, partagés en trois décades de dix jours, auxquels s'ajoutent cinq jours complémentaires, six les années sextiles.",
"Chaque jour porte le nom d'une plante, d'un animal, d'un minéral ou d'un outil des travaux des champs : le quintidi est consacré à un animal, le décadi à un outil, et en nivôse les jours honorent les minéraux.",
"Touchez le nom d'un mois pour voir sa gravure et son origine. Le jour affiché sur la page d'accueil est surligné."
],
"feastsTitle": "Jours complémentaires",
"sextileOnly": "années sextiles",
"close": "Fermer"
}
}
+50
View File
@@ -0,0 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527 713" role="img" aria-label="Jours complémentaires">
<defs>
<radialGradient id="sky" cx="50%" cy="40%" r="75%">
<stop offset="0" stop-color="#efe3c5"/>
<stop offset="1" stop-color="#d3bf93"/>
</radialGradient>
</defs>
<rect width="527" height="713" fill="url(#sky)"/>
<!-- laurel wreath -->
<g fill="none" stroke="#5f6b3c" stroke-width="3" stroke-linecap="round">
<path d="M263 640 C140 620 90 520 100 400 C108 300 150 220 210 180"/>
<path d="M263 640 C386 620 436 520 426 400 C418 300 376 220 316 180"/>
</g>
<g fill="#7b8a4c" stroke="#4d5830" stroke-width="1.5">
<g id="leaves-left">
<ellipse cx="108" cy="560" rx="11" ry="25" transform="rotate(-35 108 560)"/>
<ellipse cx="96" cy="500" rx="11" ry="25" transform="rotate(-18 96 500)"/>
<ellipse cx="94" cy="440" rx="11" ry="25" transform="rotate(-6 94 440)"/>
<ellipse cx="100" cy="380" rx="11" ry="25" transform="rotate(8 100 380)"/>
<ellipse cx="116" cy="322" rx="11" ry="25" transform="rotate(22 116 322)"/>
<ellipse cx="144" cy="270" rx="11" ry="25" transform="rotate(34 144 270)"/>
<ellipse cx="184" cy="226" rx="11" ry="25" transform="rotate(46 184 226)"/>
<ellipse cx="170" cy="580" rx="11" ry="25" transform="rotate(-60 170 580)"/>
</g>
<use href="#leaves-left" transform="translate(527 0) scale(-1 1)"/>
</g>
<!-- fasces -->
<g transform="translate(213 130) scale(1.7 1.7)" fill="none" stroke="#3a2d1d" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">
<path d="M13 50 C11 26 22 10 38 9 C47 9 52 15 47 21 C44 25 48 31 47 50 Z" fill="#8e2b26"/>
<rect x="11" y="46" width="38" height="8" rx="2" fill="#d9c9a3"/>
<rect x="27" y="54" width="6" height="14" fill="#b9a67c"/>
<g fill="#d9c9a3">
<rect x="9" y="68" width="7" height="268" rx="3.5"/>
<rect x="17" y="68" width="7" height="268" rx="3.5"/>
<rect x="25" y="68" width="7" height="268" rx="3.5"/>
<rect x="33" y="68" width="7" height="268" rx="3.5"/>
<rect x="41" y="68" width="7" height="268" rx="3.5"/>
</g>
<g fill="#c9b684">
<path d="M7 118 L51 98 L51 110 L7 130 Z"/>
<path d="M7 168 L51 148 L51 160 L7 180 Z"/>
<path d="M7 218 L51 198 L51 210 L7 230 Z"/>
<path d="M7 268 L51 248 L51 260 L7 280 Z"/>
</g>
<g fill="#3a2d1d">
<rect x="6" y="74" width="46" height="12" rx="2"/>
<rect x="6" y="318" width="46" height="12" rx="2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 360" fill="none" stroke="#3a2d1d" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">
<!-- Phrygian cap on its pike -->
<path d="M13 50 C11 26 22 10 38 9 C47 9 52 15 47 21 C44 25 48 31 47 50 Z" fill="#8e2b26"/>
<path d="M38 9 C41 6 45 7 46 10" stroke-width="1.2"/>
<rect x="11" y="46" width="38" height="8" rx="2" fill="#d9c9a3"/>
<rect x="27" y="54" width="6" height="14" fill="#b9a67c"/>
<!-- rods -->
<g fill="#d9c9a3">
<rect x="9" y="68" width="7" height="268" rx="3.5"/>
<rect x="17" y="68" width="7" height="268" rx="3.5"/>
<rect x="25" y="68" width="7" height="268" rx="3.5"/>
<rect x="33" y="68" width="7" height="268" rx="3.5"/>
<rect x="41" y="68" width="7" height="268" rx="3.5"/>
</g>
<path d="M21 72V332M29 72V332M37 72V332M45 72V332" stroke-width="0.8" opacity=".55"/>
<!-- spiral ribbon -->
<g fill="#c9b684">
<path d="M7 118 L51 98 L51 110 L7 130 Z"/>
<path d="M7 168 L51 148 L51 160 L7 180 Z"/>
<path d="M7 218 L51 198 L51 210 L7 230 Z"/>
<path d="M7 268 L51 248 L51 260 L7 280 Z"/>
</g>
<!-- binding bands -->
<g fill="#3a2d1d">
<rect x="6" y="74" width="46" height="12" rx="2"/>
<rect x="6" y="318" width="46" height="12" rx="2"/>
</g>
<rect x="6" y="342" width="46" height="8" rx="2" fill="#b9a67c"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="29" fill="#8e2b26" stroke="#3a2d1d" stroke-width="3"/>
<circle cx="32" cy="32" r="19" fill="#f3ead3"/>
<circle cx="32" cy="32" r="9" fill="#2c4268"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

+93
View File
@@ -0,0 +1,93 @@
Google Inc.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright 2011 The Old Standard Project Authors (amkryukov@gmail.com) OldStandard-Italic.ttf: Copyright 2011 The Old Standard Project Authors (amkryukov@gmail.com) OldStandard-Bold.ttf: Copyright 2011 The Old Standard Project Authors (amkryukov@gmail.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Brumaire">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<circle cx="62" cy="46" r="12" fill="#e9dcb8" opacity="0.8"/>
<g fill="none" stroke="#8a9a94" stroke-width="3" stroke-linecap="round">
<path d="M28 56 Q38 50 48 56 T68 56 T92 56"/>
<path d="M24 68 Q34 62 44 68 T64 68 T96 68"/>
<path d="M30 80 Q40 74 50 80 T70 80 T90 80"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 483 B

-9
View File
@@ -1,9 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Jours complémentaires">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g fill="none" stroke="#7a9a5a" stroke-width="3" stroke-linecap="round">
<path d="M60 90 C40 84 32 66 38 46 C46 54 48 66 60 90 Z"/>
<path d="M60 90 C80 84 88 66 82 46 C74 54 72 66 60 90 Z"/>
</g>
<path d="M60 26 L64 38 L76 38 L66 46 L70 58 L60 50 L50 58 L54 46 L44 38 L56 38 Z"
fill="#e8b84b" stroke="#a97e1f" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

-14
View File
@@ -1,14 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Floréal">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<path d="M60 68 L60 92" stroke="#5c7a4a" stroke-width="3" stroke-linecap="round"/>
<path d="M60 82 C52 80 48 74 50 70 C58 70 62 76 60 82 Z" fill="#7a9a5a" stroke="#4f6b3d" stroke-width="1.2"/>
<g fill="#c76b8a" stroke="#8a3b58" stroke-width="1.2">
<ellipse cx="60" cy="44" rx="8" ry="12"/>
<ellipse cx="60" cy="44" rx="8" ry="12" transform="rotate(60 60 44)"/>
<ellipse cx="60" cy="44" rx="8" ry="12" transform="rotate(120 60 44)"/>
<ellipse cx="60" cy="44" rx="8" ry="12" transform="rotate(180 60 44)"/>
<ellipse cx="60" cy="44" rx="8" ry="12" transform="rotate(240 60 44)"/>
<ellipse cx="60" cy="44" rx="8" ry="12" transform="rotate(300 60 44)"/>
</g>
<circle cx="60" cy="44" r="6" fill="#e8b84b" stroke="#a97e1f" stroke-width="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

-13
View File
@@ -1,13 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Frimaire">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g stroke="#6f8794" stroke-width="2.5" stroke-linecap="round">
<line x1="60" y1="32" x2="60" y2="88"/>
<line x1="32" y1="60" x2="88" y2="60"/>
<line x1="40" y1="40" x2="80" y2="80"/>
<line x1="40" y1="80" x2="80" y2="40"/>
<path d="M60 32 L54 40 M60 32 L66 40"/>
<path d="M60 88 L54 80 M60 88 L66 80"/>
<path d="M32 60 L40 54 M32 60 L40 66"/>
<path d="M88 60 L80 54 M88 60 L80 66"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fructidor">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<circle cx="50" cy="66" r="14" fill="#8a3b2b" stroke="#5c2318" stroke-width="1.5"/>
<path d="M50 52 C48 46 52 44 54 46" fill="none" stroke="#5c7a4a" stroke-width="2" stroke-linecap="round"/>
<path d="M52 46 C58 42 62 46 58 50 C54 50 52 48 52 46 Z" fill="#7a9a5a" stroke="#4f6b3d" stroke-width="1"/>
<path d="M74 74 C66 74 62 66 66 58 C74 58 78 66 74 74 Z" fill="#c9973b" stroke="#8a6b28" stroke-width="1.5"/>
<path d="M70 58 C68 52 72 50 74 52" fill="none" stroke="#5c7a4a" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Germinal">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<ellipse cx="60" cy="86" rx="7" ry="5" fill="#8a6b45" stroke="#5c4326" stroke-width="1.5"/>
<path d="M60 82 C58 68 60 56 60 44" fill="none" stroke="#5c7a4a" stroke-width="3" stroke-linecap="round"/>
<path d="M60 60 C50 56 46 48 48 40 C58 42 62 50 60 60 Z" fill="#7a9a5a" stroke="#4f6b3d" stroke-width="1.5"/>
<path d="M60 68 C70 64 74 56 72 48 C62 50 58 58 60 68 Z" fill="#7a9a5a" stroke="#4f6b3d" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

-16
View File
@@ -1,16 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Messidor">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g stroke="#c9973b" stroke-width="2.5" stroke-linecap="round" fill="none">
<path d="M60 88 L48 42"/>
<path d="M60 88 L54 40"/>
<path d="M60 88 L60 38"/>
<path d="M60 88 L66 40"/>
<path d="M60 88 L72 42"/>
<path d="M48 42 q-2 -6 4 -8"/>
<path d="M72 42 q2 -6 -4 -8"/>
<path d="M54 40 q-2 -6 4 -8"/>
<path d="M66 40 q2 -6 -4 -8"/>
<path d="M60 38 q0 -6 0 -8"/>
</g>
<path d="M46 82 Q60 74 74 82 L74 90 Q60 84 46 90 Z" fill="#8a6b45" stroke="#5c4326" stroke-width="1.2"/>
</svg>

Before

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

-14
View File
@@ -1,14 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Nivôse">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g stroke="#5b6b7a" stroke-width="2.5" stroke-linecap="round">
<line x1="60" y1="28" x2="60" y2="92"/>
<line x1="28" y1="60" x2="92" y2="60"/>
<line x1="38" y1="38" x2="82" y2="82"/>
<line x1="38" y1="82" x2="82" y2="38"/>
<path d="M60 38 L54 44 M60 38 L66 44"/>
<path d="M60 82 L54 76 M60 82 L66 76"/>
<path d="M38 60 L44 54 M38 60 L44 66"/>
<path d="M82 60 L76 54 M82 60 L76 66"/>
</g>
<circle cx="60" cy="60" r="4" fill="#5b6b7a"/>
</svg>

Before

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

-10
View File
@@ -1,10 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pluviôse">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<path d="M40 56 a12 12 0 0 1 4 -23.6 a15 15 0 0 1 29 -4 a13 13 0 0 1 8 26.6 Z"
fill="#dfe6ea" stroke="#5b6b7a" stroke-width="2"/>
<g fill="#5b7a94">
<path d="M46 76 q3 8 0 12 q-3 -4 0 -12 Z"/>
<path d="M60 80 q3 8 0 12 q-3 -4 0 -12 Z"/>
<path d="M74 76 q3 8 0 12 q-3 -4 0 -12 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

-15
View File
@@ -1,15 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Prairial">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g fill="none" stroke="#5c7a4a" stroke-width="3" stroke-linecap="round">
<path d="M36 90 C34 74 40 64 38 50"/>
<path d="M48 90 C48 72 54 64 50 48"/>
<path d="M60 90 C60 70 56 60 60 44"/>
<path d="M72 90 C72 72 66 64 70 48"/>
<path d="M84 90 C86 74 80 64 82 50"/>
</g>
<g fill="#c76b8a" stroke="#8a3b58" stroke-width="1">
<circle cx="60" cy="42" r="5"/>
<circle cx="70" cy="46" r="4"/>
<circle cx="50" cy="46" r="4"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

-14
View File
@@ -1,14 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Thermidor">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<circle cx="60" cy="60" r="16" fill="#e8b84b" stroke="#b5652b" stroke-width="2"/>
<g stroke="#b5652b" stroke-width="3" stroke-linecap="round">
<line x1="60" y1="28" x2="60" y2="38"/>
<line x1="60" y1="82" x2="60" y2="92"/>
<line x1="28" y1="60" x2="38" y2="60"/>
<line x1="82" y1="60" x2="92" y2="60"/>
<line x1="39" y1="39" x2="46" y2="46"/>
<line x1="74" y1="74" x2="81" y2="81"/>
<line x1="81" y1="39" x2="74" y2="46"/>
<line x1="46" y1="74" x2="39" y2="81"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

-16
View File
@@ -1,16 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Vendémiaire">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<path d="M60 30 C64 36 64 42 60 46" fill="none" stroke="#5c7a4a" stroke-width="2.5" stroke-linecap="round"/>
<path d="M60 34 C68 30 74 34 72 40 C66 42 61 39 60 34 Z" fill="#7a9a5a" stroke="#4f6b3d" stroke-width="1.5"/>
<g fill="#7a4a6b" stroke="#452a3a" stroke-width="1.2">
<circle cx="52" cy="56" r="6.5"/>
<circle cx="68" cy="56" r="6.5"/>
<circle cx="60" cy="52" r="6.5"/>
<circle cx="46" cy="66" r="6.5"/>
<circle cx="60" cy="64" r="6.5"/>
<circle cx="74" cy="66" r="6.5"/>
<circle cx="53" cy="76" r="6.5"/>
<circle cx="67" cy="76" r="6.5"/>
<circle cx="60" cy="86" r="6.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ventôse">
<circle cx="60" cy="60" r="54" fill="#f6efe0" stroke="#8a6b45" stroke-width="2"/>
<g fill="none" stroke="#7d8a76" stroke-width="3" stroke-linecap="round">
<path d="M26 46 C50 36 62 46 78 40"/>
<path d="M22 62 C52 50 66 64 94 54"/>
<path d="M30 78 C54 70 64 80 84 72"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 395 B

+724 -165
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"vendemiaire": ["Raisin", "Safran", "Châtaigne", "Colchique", "Cheval", "Balsamine", "Carotte", "Amaranthe", "Panais", "Cuve", "Pomme de terre", "Immortelle", "Potiron", "Réséda", "Âne", "Belle de nuit", "Citrouille", "Sarrasin", "Tournesol", "Pressoir", "Chanvre", "Pêche", "Navet", "Amaryllis", "Bœuf", "Aubergine", "Piment", "Tomate", "Orge", "Tonneau"],
"brumaire": ["Pomme", "Céleri", "Poire", "Betterave", "Oie", "Héliotrope", "Figue", "Scorsonère", "Alisier", "Charrue", "Salsifis", "Mâcre", "Topinambour", "Endive", "Dindon", "Chervis", "Cresson", "Dentelaire", "Grenade", "Herse", "Bacchante", "Azerole", "Garance", "Orange", "Faisan", "Pistache", "Macjonc", "Coing", "Cormier", "Rouleau"],
"frimaire": ["Raiponce", "Turneps", "Chicorée", "Nèfle", "Cochon", "Mâche", "Chou-fleur", "Miel", "Genièvre", "Pioche", "Cire", "Raifort", "Cèdre", "Sapin", "Chevreuil", "Ajonc", "Cyprès", "Lierre", "Sabine", "Hoyau", "Érable à sucre", "Bruyère", "Roseau", "Oseille", "Grillon", "Pignon", "Liège", "Truffe", "Olive", "Pelle"],
"nivose": ["Tourbe", "Houille", "Bitume", "Soufre", "Chien", "Lave", "Terre végétale", "Fumier", "Salpêtre", "Fléau", "Granit", "Argile", "Ardoise", "Grès", "Lapin", "Silex", "Marne", "Pierre à chaux", "Marbre", "Van", "Pierre à plâtre", "Sel", "Fer", "Cuivre", "Chat", "Étain", "Plomb", "Zinc", "Mercure", "Crible"],
"pluviose": ["Lauréole", "Mousse", "Fragon", "Perce-neige", "Taureau", "Laurier-thym", "Amadouvier", "Mézéréon", "Peuplier", "Coignée", "Ellébore", "Brocoli", "Laurier", "Avelinier", "Vache", "Buis", "Lichen", "If", "Pulmonaire", "Serpette", "Thlaspi", "Thimelé", "Chiendent", "Trainasse", "Lièvre", "Guède", "Noisetier", "Cyclamen", "Chélidoine", "Traîneau"],
"ventose": ["Tussilage", "Cornouiller", "Violier", "Troène", "Bouc", "Asaret", "Alaterne", "Violette", "Marceau", "Bêche", "Narcisse", "Orme", "Fumeterre", "Vélar", "Chèvre", "Épinard", "Doronic", "Mouron", "Cerfeuil", "Cordeau", "Mandragore", "Persil", "Cochléaria", "Pâquerette", "Thon", "Pissenlit", "Sylve", "Capillaire", "Frêne", "Plantoir"],
"germinal": ["Primevère", "Platane", "Asperge", "Tulipe", "Poule", "Bette", "Bouleau", "Jonquille", "Aulne", "Couvoir", "Pervenche", "Charme", "Morille", "Hêtre", "Abeille", "Laitue", "Mélèze", "Ciguë", "Radis", "Ruche", "Gainier", "Romaine", "Marronnier", "Roquette", "Pigeon", "Lilas", "Anémone", "Pensée", "Myrtille", "Greffoir"],
"floreal": ["Rose", "Chêne", "Fougère", "Aubépine", "Rossignol", "Ancolie", "Muguet", "Champignon", "Hyacinthe", "Râteau", "Rhubarbe", "Sainfoin", "Bâton-d'or", "Chamérisier", "Ver à soie", "Consoude", "Pimprenelle", "Corbeille d'or", "Arroche", "Sarcloir", "Statice", "Fritillaire", "Bourrache", "Valériane", "Carpe", "Fusain", "Civette", "Buglosse", "Sénevé", "Houlette"],
"prairial": ["Luzerne", "Hémérocalle", "Trèfle", "Angélique", "Canard", "Mélisse", "Fromental", "Martagon", "Serpolet", "Faux", "Fraise", "Bétoine", "Pois", "Acacia", "Caille", "Œillet", "Sureau", "Pavot", "Tilleul", "Fourche", "Barbeau", "Camomille", "Chèvrefeuille", "Caille-lait", "Tanche", "Jasmin", "Verveine", "Thym", "Pivoine", "Chariot"],
"messidor": ["Seigle", "Avoine", "Oignon", "Véronique", "Mulet", "Romarin", "Concombre", "Échalote", "Absinthe", "Faucille", "Coriandre", "Artichaut", "Girofle", "Lavande", "Chamois", "Tabac", "Groseille", "Gesse", "Cerise", "Parc", "Menthe", "Cumin", "Haricot", "Orcanète", "Pintade", "Sauge", "Ail", "Vesce", "Blé", "Chalémie"],
"thermidor": ["Épeautre", "Bouillon-blanc", "Melon", "Ivraie", "Bélier", "Prêle", "Armoise", "Carthame", "Mûre", "Arrosoir", "Panic", "Salicorne", "Abricot", "Basilic", "Brebis", "Guimauve", "Lin", "Amande", "Gentiane", "Écluse", "Carline", "Câprier", "Lentille", "Aunée", "Loutre", "Myrte", "Colza", "Lupin", "Coton", "Moulin"],
"fructidor": ["Prune", "Millet", "Lycoperdon", "Escourgeon", "Saumon", "Tubéreuse", "Sucrion", "Apocyn", "Réglisse", "Échelle", "Pastèque", "Fenouil", "Épine-vinette", "Noix", "Truite", "Citron", "Cardère", "Nerprun", "Tagète", "Hotte", "Églantier", "Noisette", "Houblon", "Sorgho", "Écrevisse", "Bigarade", "Verge d'or", "Maïs", "Marron", "Panier"]
}
+56 -13
View File
@@ -4,16 +4,15 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title data-i18n="site.title">Calendrier républicain</title>
<link rel="icon" href="data:,">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="preload" href="assets/fonts/old-standard-tt-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="assets/fonts/im-fell-english-sc.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header class="site-header">
<a class="brand" href="#/">
<span class="brand-mark">an</span>
<span class="cockade" aria-hidden="true"></span>
<span class="brand-title" data-i18n="site.title">Calendrier républicain</span>
</a>
<div class="header-actions">
@@ -29,6 +28,7 @@
<nav id="main-nav" class="main-nav" aria-hidden="true">
<a href="#/" class="nav-link" data-view="home" data-i18n="nav.home">Accueil</a>
<a href="#/calendar" class="nav-link" data-view="calendar" data-i18n="nav.calendar">Le calendrier complet</a>
<p class="nav-section-label" data-i18n="nav.about">À propos</p>
<a href="#/about/history" class="nav-link" data-view="history" data-i18n="nav.aboutHistory">Histoire du calendrier</a>
<a href="#/about/leap-years" class="nav-link" data-view="leap-years" data-i18n="nav.aboutLeapYears">Les années sextiles</a>
@@ -38,16 +38,26 @@
<main id="app">
<section id="view-home" class="view">
<h1 class="today-heading">
<span data-i18n="home.todayHeading">Nous sommes aujourd'hui le</span>
</h1>
<div class="tablet">
<header class="tablet-head">
<h1 class="today-heading" data-i18n="home.todayHeading">Nous sommes aujourd'hui le</h1>
<div class="ornament" aria-hidden="true"></div>
</header>
<div class="calendar-card">
<div class="month-illustration">
<img id="month-image" src="assets/months/vendemiaire.svg" alt="" width="140" height="140">
<p id="month-period" class="month-period"></p>
<div class="tablet-body">
<figure class="plate">
<div class="plate-frame">
<img id="month-image" src="assets/months/vendemiaire.jpg" alt="" width="527" height="713">
</div>
<figcaption>
<h2 id="month-name" class="month-name">—</h2>
<p id="month-period" class="month-period"></p>
</figcaption>
</figure>
<img class="fasces" src="assets/fasces.svg" alt="" aria-hidden="true">
<div class="decree">
<div class="date-display">
<p id="decade-day" class="decade-day">—</p>
<p id="republican-date" class="republican-date">—</p>
@@ -59,13 +69,16 @@
</p>
</div>
<div class="ornament" aria-hidden="true"></div>
<div class="description-panel">
<h2 id="month-name" class="month-name">—</h2>
<p id="month-season-suffix" class="month-season-suffix"></p>
<p id="month-etymology" class="month-etymology"></p>
<p id="month-description" class="month-description"></p>
</div>
</div>
</div>
</div>
<form id="convert-form" class="convert-form">
<h2 data-i18n="form.title">Convertir une autre date</h2>
@@ -80,13 +93,43 @@
</form>
</section>
<section id="view-calendar" class="view" hidden>
<div class="poster">
<header class="poster-head">
<h1 data-i18n="calendar.title">Calendrier de la République française</h1>
<p class="poster-subtitle" data-i18n="calendar.subtitle">Les douze mois, les trois décades et les jours de l'année</p>
<div class="ornament" aria-hidden="true"></div>
</header>
<div id="poster-grid" class="poster-grid"></div>
<div id="poster-feasts" class="poster-feasts"></div>
</div>
</section>
<section id="view-about" class="view about-view" hidden>
<h1 id="about-title">—</h1>
<div id="about-content" class="about-content"></div>
</section>
</main>
<dialog id="month-dialog" class="month-dialog">
<form method="dialog">
<button class="dialog-close" aria-label="Fermer" data-i18n-aria-label="calendar.close">×</button>
</form>
<figure class="plate">
<div class="plate-frame"><img id="dialog-image" src="" alt="" width="527" height="713"></div>
<figcaption>
<h2 id="dialog-name" class="month-name"></h2>
<p id="dialog-period" class="month-period"></p>
</figcaption>
</figure>
<div class="dialog-text">
<p id="dialog-etymology" class="month-etymology"></p>
<p id="dialog-description" class="month-description"></p>
</div>
</dialog>
<footer class="site-footer">
<div class="ornament" aria-hidden="true"></div>
<p data-i18n="footer.text">Dates calculées selon la règle arithmétique perpétuelle proposée par Gilbert Romme (19 floréal an III / 8 mai 1795).</p>
</footer>
+222 -8
View File
@@ -19,10 +19,20 @@
const formError = document.getElementById('form-error');
const viewHome = document.getElementById('view-home');
const viewCalendar = document.getElementById('view-calendar');
const viewAbout = document.getElementById('view-about');
const aboutTitle = document.getElementById('about-title');
const aboutContent = document.getElementById('about-content');
const posterGrid = document.getElementById('poster-grid');
const posterFeasts = document.getElementById('poster-feasts');
const monthDialog = document.getElementById('month-dialog');
const dialogImage = document.getElementById('dialog-image');
const dialogName = document.getElementById('dialog-name');
const dialogPeriod = document.getElementById('dialog-period');
const dialogEtymology = document.getElementById('dialog-etymology');
const dialogDescription = document.getElementById('dialog-description');
const menuToggle = document.getElementById('menu-toggle');
const mainNav = document.getElementById('main-nav');
const navBackdrop = document.getElementById('nav-backdrop');
@@ -34,7 +44,24 @@
'about/complementary-days': 'complementaryDays',
};
// Season columns of the full calendar; `image` is the month plate shown in the arch,
// `focus` its vertical crop position so that the face stays in the arch.
const SEASONS = [
{ key: 'autumn', image: 'vendemiaire', focus: '23%', months: ['vendemiaire', 'brumaire', 'frimaire'] },
{ key: 'winter', image: 'nivose', focus: '18%', months: ['nivose', 'pluviose', 'ventose'] },
{ key: 'spring', image: 'floreal', focus: '18%', months: ['germinal', 'floreal', 'prairial'] },
{ key: 'summer', image: 'messidor', focus: '18%', months: ['messidor', 'thermidor', 'fructidor'] },
];
const DECADE_DAY_KEYS = [
'primidi', 'duodi', 'tridi', 'quartidi', 'quintidi',
'sextidi', 'septidi', 'octidi', 'nonidi', 'decadi',
];
const COMPLEMENTARY_DAY_KEYS = ['virtue', 'genius', 'labour', 'opinion', 'rewards', 'revolution'];
let lastRepublicanResult = null;
let dayNames = {};
function pad(value) {
return String(value).padStart(2, '0');
@@ -49,6 +76,13 @@
return `${weekdayLabel} ${pad(day)}/${pad(month)}/${year}`.trim();
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
function closeMenu() {
mainNav.classList.remove('open');
mainNav.setAttribute('aria-hidden', 'true');
@@ -77,7 +111,7 @@
const day = complementary.days[result.complementaryDayKey] || {};
decadeDay.textContent = complementary.title || '';
republicanDate.textContent = day.name || '';
monthImage.src = 'assets/months/complementary.svg';
monthImage.src = 'assets/complementary.svg';
monthImage.alt = day.name || '';
monthPeriod.textContent = '';
monthName.textContent = day.name || '';
@@ -89,7 +123,7 @@
const seasonSuffix = (I18N.months.seasonSuffix || {})[month.season] || '';
decadeDay.textContent = decadeDays[result.decadeDayKey] || '';
republicanDate.textContent = `${result.dayOfMonth} ${month.name || ''}`.trim();
monthImage.src = `assets/months/${result.monthKey}.svg`;
monthImage.src = `assets/months/${result.monthKey}.jpg`;
monthImage.alt = month.name || '';
monthPeriod.textContent = month.period || '';
monthName.textContent = month.name || '';
@@ -97,6 +131,167 @@
monthEtymology.textContent = month.etymology || '';
monthDescription.textContent = month.description || '';
}
markToday();
}
/** Highlights the displayed republican date inside the full calendar. */
function markToday() {
posterGrid.querySelectorAll('.today, .today-row, .is-current').forEach((element) => {
element.classList.remove('today', 'today-row', 'is-current');
});
posterFeasts.querySelectorAll('.today').forEach((element) => element.classList.remove('today'));
const result = lastRepublicanResult;
if (!result) return;
if (result.isComplementaryDay) {
const feast = posterFeasts.querySelector(`[data-feast="${result.complementaryDayKey}"]`);
if (feast) feast.classList.add('today');
return;
}
const cell = posterGrid.querySelector(
`td[data-month="${result.monthKey}"][data-day="${result.dayOfMonth}"]`
);
if (cell) {
cell.classList.add('today');
cell.parentElement.firstElementChild.classList.add('today-row');
}
const monthButton = posterGrid.querySelector(`.month-button[data-month="${result.monthKey}"]`);
if (monthButton) monthButton.classList.add('is-current');
}
function buildSeasonTable(season) {
const months = I18N.months.months || {};
const decadeDays = I18N.months.decadeDays || {};
const decades = I18N.t('calendar.decades', []);
const table = createElement('table', 'season-table');
const colgroup = table.appendChild(document.createElement('colgroup'));
colgroup.appendChild(createElement('col', 'col-day'));
season.months.forEach(() => colgroup.appendChild(document.createElement('col')));
const headRow = table.createTHead().insertRow();
headRow.appendChild(createElement('th', 'day-head', I18N.t('calendar.dayColumn')));
season.months.forEach((monthKey) => {
const th = createElement('th');
th.scope = 'col';
const button = createElement('button', 'month-button', (months[monthKey] || {}).name || monthKey);
button.type = 'button';
button.dataset.month = monthKey;
th.appendChild(button);
headRow.appendChild(th);
});
const body = table.createTBody();
for (let decade = 0; decade < 3; decade += 1) {
const decadeRow = body.insertRow();
decadeRow.className = 'decade-row';
const decadeHead = createElement('th', '', decades[decade] || '');
decadeHead.colSpan = season.months.length + 1;
decadeRow.appendChild(decadeHead);
for (let index = 0; index < 10; index += 1) {
const day = decade * 10 + index + 1;
const row = body.insertRow();
if (index === 9) row.className = 'decade-end';
const dayHead = createElement('th');
dayHead.scope = 'row';
dayHead.appendChild(createElement('span', 'day-number', String(day)));
const decadeDayName = decadeDays[DECADE_DAY_KEYS[index]] || '';
const abbreviation = createElement('abbr', '', `${decadeDayName.slice(0, 3)}.`);
abbreviation.title = decadeDayName;
dayHead.appendChild(abbreviation);
row.appendChild(dayHead);
season.months.forEach((monthKey) => {
const cell = createElement('td', '', (dayNames[monthKey] || [])[day - 1] || '');
cell.dataset.month = monthKey;
cell.dataset.day = String(day);
row.appendChild(cell);
});
}
}
return table;
}
function buildSeason(season) {
const section = createElement('section', 'season');
const arch = createElement('div', 'arch');
const image = createElement('img');
image.src = `assets/months/${season.image}.jpg`;
image.alt = '';
image.loading = 'lazy';
image.style.objectPosition = `50% ${season.focus}`;
arch.appendChild(image);
section.appendChild(arch);
section.appendChild(createElement('h2', 'season-title', I18N.t(`calendar.seasons.${season.key}`)));
section.appendChild(buildSeasonTable(season));
return section;
}
function buildPosterCenter() {
const center = createElement('aside', 'poster-center');
const medallion = createElement('div', 'medallion');
const inner = createElement('div', 'medallion-inner');
const fasces = createElement('img');
fasces.src = 'assets/fasces.svg';
fasces.alt = '';
inner.appendChild(fasces);
medallion.appendChild(inner);
center.appendChild(medallion);
const explanation = createElement('div', 'explanation');
explanation.appendChild(createElement('h2', '', I18N.t('calendar.explanationTitle')));
I18N.t('calendar.explanation', []).forEach((paragraph) => {
explanation.appendChild(createElement('p', '', paragraph));
});
center.appendChild(explanation);
return center;
}
function buildFeasts() {
const complementary = I18N.months.complementaryDays || { days: {} };
const decadeDays = I18N.months.decadeDays || {};
posterFeasts.replaceChildren();
COMPLEMENTARY_DAY_KEYS.forEach((key, index) => {
const feast = createElement('p', 'feast');
feast.dataset.feast = key;
feast.appendChild(createElement('span', 'feast-day', decadeDays[DECADE_DAY_KEYS[index]] || ''));
feast.appendChild(createElement('span', 'feast-name', (complementary.days[key] || {}).name || ''));
if (key === 'revolution') {
feast.appendChild(createElement('span', 'feast-note', I18N.t('calendar.sextileOnly')));
}
posterFeasts.appendChild(feast);
});
}
function renderCalendarView() {
posterGrid.replaceChildren(...SEASONS.map(buildSeason), buildPosterCenter());
buildFeasts();
markToday();
}
function openMonthDialog(monthKey) {
const month = (I18N.months.months || {})[monthKey];
if (!month) return;
dialogImage.src = `assets/months/${monthKey}.jpg`;
dialogImage.alt = month.name;
dialogName.textContent = month.name;
dialogPeriod.textContent = month.period || '';
dialogEtymology.textContent = month.etymology || '';
dialogDescription.textContent = month.description || '';
monthDialog.showModal();
}
async function loadDayNames() {
try {
const response = await fetch('data/day-names.json');
dayNames = await response.json();
} catch (error) {
dayNames = {};
}
}
function showFormError(messageKey) {
@@ -170,18 +365,23 @@
document.querySelectorAll('.nav-link').forEach((link) => link.classList.remove('active'));
if (hash in ABOUT_ROUTES) {
viewHome.hidden = true;
viewCalendar.hidden = true;
viewAbout.hidden = true;
let activeHref = '#/';
if (hash === 'calendar') {
viewCalendar.hidden = false;
activeHref = '#/calendar';
} else if (hash in ABOUT_ROUTES) {
viewAbout.hidden = false;
renderAboutView(ABOUT_ROUTES[hash]);
const activeLink = document.querySelector(`.nav-link[href="#/${hash}"]`);
if (activeLink) activeLink.classList.add('active');
activeHref = `#/${hash}`;
} else {
viewHome.hidden = false;
viewAbout.hidden = true;
const activeLink = document.querySelector('.nav-link[href="#/"]');
if (activeLink) activeLink.classList.add('active');
}
const activeLink = document.querySelector(`.nav-link[href="${activeHref}"]`);
if (activeLink) activeLink.classList.add('active');
}
function bindEvents() {
@@ -195,6 +395,18 @@
navBackdrop.addEventListener('click', closeMenu);
window.addEventListener('hashchange', route);
posterGrid.addEventListener('click', (event) => {
const button = event.target.closest('.month-button');
if (button) openMonthDialog(button.dataset.month);
});
// A click on the ::backdrop targets the dialog itself, outside its box.
monthDialog.addEventListener('click', (event) => {
const box = monthDialog.getBoundingClientRect();
const inside = event.clientX >= box.left && event.clientX <= box.right
&& event.clientY >= box.top && event.clientY <= box.bottom;
if (!inside) monthDialog.close();
});
convertForm.addEventListener('submit', handleConvertSubmit);
todayButton.addEventListener('click', () => {
loadToday();
@@ -207,6 +419,7 @@
I18N.onChange((lang) => {
langSwitch.textContent = lang.toUpperCase();
renderCalendarView();
if (lastRepublicanResult) {
renderRepublicanResult(lastRepublicanResult);
}
@@ -220,6 +433,7 @@
async function init() {
bindEvents();
const initialLang = I18N.detectInitialLang();
await loadDayNames();
await I18N.load(initialLang);
langSwitch.textContent = initialLang.toUpperCase();
route();
+6
View File
@@ -39,6 +39,12 @@ const I18N = (() => {
element.textContent = value;
}
});
document.querySelectorAll('[data-i18n-aria-label]').forEach((element) => {
const value = getByPath(common, element.getAttribute('data-i18n-aria-label'));
if (value !== undefined) {
element.setAttribute('aria-label', value);
}
});
}
async function fetchJson(url) {
+40
View File
@@ -0,0 +1,40 @@
'use strict';
/**
* Crops the month plates in src/images/ (removes the paper margin, the thin
* frame and the printed caption) and writes web-sized JPGs to
* public/assets/months/<monthKey>.jpg.
*
* Not a project dependency: run with `npm i --no-save sharp && node scripts/prepare-images.cjs`.
*/
const fs = require('node:fs');
const path = require('node:path');
const sharp = require('sharp');
const SRC_DIR = path.join(__dirname, '..', 'src', 'images');
const OUT_DIR = path.join(__dirname, '..', 'public', 'assets', 'months');
// Pixels to remove around the engraving (the scans differ by a few pixels in width).
const CROP = { left: 62, top: 62, right: 66, bottom: 225 };
const OUTPUT_WIDTH = 560;
async function main() {
fs.mkdirSync(OUT_DIR, { recursive: true });
for (const file of fs.readdirSync(SRC_DIR).filter((name) => name.endsWith('.jpg'))) {
const source = path.join(SRC_DIR, file);
const { width, height } = await sharp(source).metadata();
await sharp(source)
.extract({
left: CROP.left,
top: CROP.top,
width: width - CROP.left - CROP.right,
height: height - CROP.top - CROP.bottom,
})
.resize({ width: OUTPUT_WIDTH })
.jpeg({ quality: 80, mozjpeg: true })
.toFile(path.join(OUT_DIR, file.toLowerCase()));
}
}
main();
+11 -1
View File
@@ -17,9 +17,15 @@ const MIME_TYPES = {
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.woff2': 'font/woff2',
'.txt': 'text/plain; charset=utf-8',
'.ico': 'image/x-icon',
};
// Binary assets never change without a rename, so they can be cached for a day.
const CACHEABLE_EXTENSIONS = new Set(['.jpg', '.woff2', '.svg']);
function sendJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
@@ -78,7 +84,11 @@ function serveStatic(res, rootDir, requestPath) {
return;
}
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
const headers = { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' };
if (CACHEABLE_EXTENSIONS.has(ext)) {
headers['Cache-Control'] = 'public, max-age=86400';
}
res.writeHead(200, headers);
res.end(content);
});
}