feat: Building the app in it's first version

This commit is contained in:
gauvainboiche
2026-09-15 23:12:57 +02:00
parent 34792e2782
commit 64a3d9b556
42 changed files with 2699 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
npm-debug.log*
.env
+16
View File
@@ -0,0 +1,16 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --omit=dev
COPY server.js ./
COPY src ./src
COPY public ./public
COPY locales ./locales
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]
+11
View File
@@ -0,0 +1,11 @@
services:
calendrier-republicain:
build:
context: .
dockerfile: Dockerfile
container_name: calendrier-republicain
ports:
- "3000:3000"
environment:
- PORT=3000
restart: unless-stopped
+74
View File
@@ -0,0 +1,74 @@
{
"history": {
"title": "History of the Republican calendar",
"sections": [
{
"heading": "A revolutionary creation",
"paragraphs": [
"The French Republican calendar was created by the National Convention during the French Revolution. It came into force on 15 Vendémiaire, Year II (6 October 1793), with a retroactive start date of 1 Vendémiaire, Year I — 22 September 1792, the day after the abolition of the monarchy, proclaimed the first day of the \"era of the French\".",
"Like the metric system, the calendar grew out of the revolutionaries' wish to base the measurement of time on the decimal system and to replace the Gregorian calendar, associated with Christianity and the Ancien Régime.",
"The year was split into twelve thirty-day months, grouped into three ten-day décades, replacing the seven-day week and its Sunday with a ten-day rest cycle. Five complementary days (six in sextile years) closed out the year outside of any month."
]
},
{
"heading": "The Romme commission and Fabre d'Églantine",
"paragraphs": [
"Tasked by the Convention with reforming the calendar, deputy Gilbert Romme worked with several members of the Academy of Sciences (Lagrange, Monge, Lalande, Pingré...). His report, presented on 20 September 1793, laid out the calendar's core principles: a year anchored to the autumnal equinox, a decimal division of time, and twelve thirty-day months.",
"The poetic month names — Vendémiaire, Brumaire, Frimaire, Nivôse, Pluviôse, Ventôse, Germinal, Floréal, Prairial, Messidor, Thermidor, Fructidor — were coined by the poet and deputy Fabre d'Églantine and approved by the Convention on 24 October 1793. Each day was further associated with an agricultural product or a farming tool."
]
},
{
"heading": "A short but memorable life",
"paragraphs": [
"The Republican calendar remained in force for just over twelve years, until it was abolished by Napoleon I effective 1 January 1806, for practical as much as political and religious reasons.",
"It was briefly revived by the Paris Commune in 1871, and left a lasting mark on the French language through several given names (Nivôse, Floréal...) and the expression \"sans-culotte\"."
]
}
]
},
"leapYears": {
"title": "Sextile years",
"sections": [
{
"heading": "A contradiction in the founding decree",
"paragraphs": [
"The decree of 4 Frimaire, Year II (24 November 1793) set two hard-to-reconcile rules: the year begins on the day of the true autumnal equinox at the Paris Observatory (Article III), and a sextile year — the Republican equivalent of a leap year — is inserted every four years (Article X).",
"In reality, the interval between two successive equinoxes is not perfectly constant. During the calendar's actual period of use (Year I to Year XIV), sextile years did fall every four years (Year III, Year VII, Year XI), but astronomers already knew this regularity would not hold indefinitely."
]
},
{
"heading": "Romme's reform proposal",
"paragraphs": [
"On 19 Floréal, Year III (8 May 1795), Romme proposed to settle the contradiction by dropping the reference to the observed equinox in favour of a predictable, perpetual arithmetic rule: a sextile year every four years (the first being Year IV), except centennial years — which stay common unless their number is divisible by 400 — with the exception of the fortieth century, which remains common.",
"This rule closely mirrors the Gregorian leap-year rule, simply shifted by one year. Romme was arrested shortly afterwards and took his own life in prison, so his reform was never officially adopted — but it remains the reference for any perpetual Republican calendar, and it is the one used by this converter."
]
},
{
"heading": "The rule used here",
"paragraphs": [
"A Republican year is therefore sextile (366 days) if its number is divisible by 4, unless it is divisible by 100 without being divisible by 400, and unless it is divisible by 4000. Every other year has 365 days, split into twelve thirty-day months plus five complementary days."
]
}
]
},
"complementaryDays": {
"title": "The complementary days",
"sections": [
{
"heading": "Closing out the year",
"paragraphs": [
"Twelve thirty-day months only add up to 360 days: five more days are needed to complete a tropical year, six in sextile years. These days, first called \"épagomènes\" and then \"sans-culottides\" by the decree of 4 Frimaire, Year II, were renamed \"complementary days\" by the decree of 7 Fructidor, Year III (24 August 1795).",
"Sitting outside any month or décade, they carry no décadi name and were devoted to public rejoicing and festivities marking the end of the Republican year, roughly between 17 and 22 September on the Gregorian calendar."
]
},
{
"heading": "The five (or six) days",
"paragraphs": [
"The Day of Virtue, the Day of Genius, the Day of Labour, the Day of Opinion and the Day of Rewards make up the five complementary days of every Republican year.",
"In sextile years — every four years under Romme's perpetual rule — a sixth day is added, the Day of the Revolution, devoted to national games in memory of the Republic's founding.",
"The four-year span between two sextile years is called a \"Franciade\", echoing the Olympiads of ancient Greece."
]
}
]
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"site": {
"title": "Republican Calendar",
"tagline": "Perpetual converter"
},
"nav": {
"menuLabel": "Menu",
"home": "Home",
"about": "About",
"aboutHistory": "History of the calendar",
"aboutLeapYears": "Sextile years",
"aboutComplementaryDays": "The complementary days",
"language": "Language"
},
"home": {
"todayHeading": "Today is",
"resultHeading": "Converted date",
"gregorianEquivalent": "Gregorian equivalent",
"sextileBadge": "Sextile year",
"decadeLabel": "Décade",
"dayOfYearLabel": "Day of the year",
"yearLabel": "Year"
},
"form": {
"title": "Convert another date",
"dateLabel": "Gregorian date (DD/MM/YYYY)",
"submit": "Convert",
"todayButton": "Back to today",
"helpText": "Pick a day, month and year: the form checks automatically that the date actually exists.",
"errorInvalid": "This date does not exist in the Gregorian calendar.",
"errorBeforeEpoch": "The Republican calendar starts on 22 September 1792 (1 Vendémiaire, Year I). Please choose a later date.",
"errorGeneric": "Something went wrong while converting this date."
},
"weekdays": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"footer": {
"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…"
}
+135
View File
@@ -0,0 +1,135 @@
{
"seasonSuffix": {
"autumn": "Ending in \"-aire\" (from Latin -arius)",
"winter": "Ending in \"-ôse\" (from Latin -osus, \"endowed with\")",
"spring": "Ending in \"-al\" (from Latin -alis)",
"summer": "Ending in \"-idor\" (from Greek dôron, \"gift\")"
},
"months": {
"vendemiaire": {
"name": "Vendémiaire",
"season": "autumn",
"period": "22/23/24 September 21/22/23 October",
"etymology": "From Latin vindemia, \"grape harvest\".",
"description": "The first month of the Republican year opens autumn and starts on equinox day. Fabre d'Églantine named it after the grape harvest that closes the work of the vineyard."
},
"brumaire": {
"name": "Brumaire",
"season": "autumn",
"period": "22/23/24 October 20/21/22 November",
"etymology": "From Latin bruma, \"fog, mist\".",
"description": "The month of the mists and fog that blanket the countryside between October and November."
},
"frimaire": {
"name": "Frimaire",
"season": "autumn",
"period": "21/22/23 November 20/21/22 December",
"etymology": "From the French word \"frimas\", the damp cold of late autumn.",
"description": "The last month of autumn, marked by the first frosts, alternately dry and humid, heralding winter."
},
"nivose": {
"name": "Nivôse",
"season": "winter",
"period": "21/22/23 December 19/20/21 January",
"etymology": "From Latin nix, nivis, \"snow\".",
"description": "The first month of winter, named for the snow that whitens the ground between December and January."
},
"pluviose": {
"name": "Pluviôse",
"season": "winter",
"period": "20/21/22 January 18/19/20 February",
"etymology": "From Latin pluvia, \"rain\".",
"description": "The month of abundant rainfall typically falling between January and February."
},
"ventose": {
"name": "Ventôse",
"season": "winter",
"period": "19/20/21 February 20/21 March",
"etymology": "From Latin ventosus, \"windy\".",
"description": "The last month of winter, that of squalls and drying winds between February and March."
},
"germinal": {
"name": "Germinal",
"season": "spring",
"period": "21/22 March 19/20 April",
"etymology": "From Latin germen, \"sprout, germ\".",
"description": "The first month of spring, that of budding and rising sap between March and April."
},
"floreal": {
"name": "Floréal",
"season": "spring",
"period": "20/21 April 19/20 May",
"etymology": "From Latin flos, floris, \"flower\".",
"description": "The month when flowers bloom, between April and May."
},
"prairial": {
"name": "Prairial",
"season": "spring",
"period": "20/21 May 18/19 June",
"etymology": "From the French word \"prairie\", meadow.",
"description": "The last month of spring, that of cheerful abundance and the meadow harvest between May and June."
},
"messidor": {
"name": "Messidor",
"season": "summer",
"period": "19/20 June 18/19 July",
"etymology": "From Latin messis, \"harvest\", and Greek dôron, \"gift\".",
"description": "The first month of summer, that of rippling ears of wheat and golden harvests between June and July."
},
"thermidor": {
"name": "Thermidor",
"season": "summer",
"period": "19/20 July 17/18 August",
"etymology": "From Greek thermos, \"heat\", and dôron, \"gift\".",
"description": "The month of heat, both solar and terrestrial, that sets the air ablaze between July and August."
},
"fructidor": {
"name": "Fructidor",
"season": "summer",
"period": "18/19 August 16/17 September",
"etymology": "From Latin fructus, \"fruit\", and Greek dôron, \"gift\".",
"description": "The last month of the year, that of the fruit ripened golden by the sun between August and September."
}
},
"decadeDays": {
"primidi": "Primidi",
"duodi": "Duodi",
"tridi": "Tridi",
"quartidi": "Quartidi",
"quintidi": "Quintidi",
"sextidi": "Sextidi",
"septidi": "Septidi",
"octidi": "Octidi",
"nonidi": "Nonidi",
"decadi": "Décadi"
},
"complementaryDays": {
"title": "Complementary days (sans-culottides)",
"days": {
"virtue": {
"name": "Day of Virtue",
"description": "First complementary day, dedicated to civic virtue."
},
"genius": {
"name": "Day of Genius",
"description": "Second complementary day, celebrating genius and inventiveness."
},
"labour": {
"name": "Day of Labour",
"description": "Third complementary day, honouring work."
},
"opinion": {
"name": "Day of Opinion",
"description": "Fourth complementary day, devoted to freedom of opinion."
},
"rewards": {
"name": "Day of Rewards",
"description": "Fifth complementary day, dedicated to public rewards."
},
"revolution": {
"name": "Day of the Revolution",
"description": "Sixth complementary day, added only in sextile years, commemorating the Revolution with national games."
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
{
"history": {
"title": "Histoire du calendrier républicain",
"sections": [
{
"heading": "Une création révolutionnaire",
"paragraphs": [
"Le calendrier républicain, ou calendrier révolutionnaire français, est créé par la Convention nationale au cours de la Révolution française. Il entre en vigueur le 15 vendémiaire an II (6 octobre 1793), avec pour date initiale rétroactive le 1er vendémiaire an I, soit le 22 septembre 1792 — lendemain de l'abolition de la monarchie, proclamé premier jour de l'« ère des Français ».",
"Comme le système métrique, ce calendrier procède de la volonté des révolutionnaires de fonder la mesure du temps sur le système décimal et de remplacer le calendrier grégorien, associé au christianisme et à l'Ancien Régime.",
"L'année y est découpée en douze mois de trente jours, regroupés en trois décades de dix jours chacune, remplaçant ainsi la semaine de sept jours et son dimanche par un jour de repos décadaire. Cinq jours complémentaires (six les années sextiles) closent l'année en dehors des mois."
]
},
{
"heading": "La commission Romme et Fabre d'Églantine",
"paragraphs": [
"Chargé par la Convention d'étudier la réforme du calendrier, le député Gilbert Romme s'entoure de plusieurs membres de l'Académie des sciences (Lagrange, Monge, Lalande, Pingré...). Son rapport, présenté le 20 septembre 1793, pose les grands principes du nouveau calendrier : une année calée sur l'équinoxe d'automne, une division décimale du temps et douze mois de trente jours.",
"Les noms poétiques des mois — Vendémiaire, Brumaire, Frimaire, Nivôse, Pluviôse, Ventôse, Germinal, Floréal, Prairial, Messidor, Thermidor, Fructidor — sont l'œuvre du poète et député Fabre d'Églantine, approuvés par la Convention le 24 octobre 1793. Chaque jour du calendrier y est en outre associé à une production agricole ou un outil rural."
]
},
{
"heading": "Un usage bref mais marquant",
"paragraphs": [
"Le calendrier républicain reste en vigueur un peu plus de douze ans, jusqu'à son abrogation par Napoléon Ier à compter du 1er janvier 1806, pour des raisons pratiques autant que politiques et religieuses.",
"Il sera brièvement remis en usage par la Commune de Paris en 1871, et laissera une trace durable dans la langue française à travers plusieurs prénoms (Nivôse, Floréal...) et l'expression « sans-culotte »."
]
}
]
},
"leapYears": {
"title": "Les années sextiles",
"sections": [
{
"heading": "Une contradiction dans le décret fondateur",
"paragraphs": [
"Le décret du 4 frimaire an II (24 novembre 1793) fixe deux règles difficilement compatibles : l'année commence le jour de l'équinoxe d'automne vrai à l'Observatoire de Paris (article III), et l'année sextile — l'équivalent républicain de l'année bissextile — est intercalée tous les quatre ans (article X).",
"Or l'intervalle réel entre deux équinoxes successifs n'est pas rigoureusement constant : durant la période d'usage effectif du calendrier (an I à an XIV), les années sextiles tombèrent bien tous les quatre ans (an III, an VII, an XI), mais les astronomes savaient déjà que cette régularité ne durerait pas indéfiniment."
]
},
{
"heading": "Le projet de réforme de Romme",
"paragraphs": [
"Le 19 floréal an III (8 mai 1795), Romme propose de trancher la contradiction en abandonnant la référence à l'équinoxe observé, au profit d'une règle arithmétique et perpétuement prévisible : une année sextile tous les quatre ans (la première étant l'an IV), sauf les années séculaires — non sextiles sauf si leur millésime est divisible par 400 — à l'exception du quarantième siècle, qui reste commun.",
"Cette règle est directement calquée sur celle du calendrier grégorien, simplement décalée d'une unité. Romme est arrêté peu après et se suicide en prison ; sa réforme ne sera donc jamais adoptée officiellement, mais elle reste la référence pour tout calendrier républicain perpétuel — c'est celle utilisée par ce convertisseur."
]
},
{
"heading": "La règle appliquée ici",
"paragraphs": [
"Une année républicaine est donc sextile (366 jours) si son millésime est divisible par 4, sauf s'il est divisible par 100 sans l'être par 400, et sauf s'il est divisible par 4000. Toutes les autres années comptent 365 jours, réparties en douze mois de trente jours et cinq jours complémentaires."
]
}
]
},
"complementaryDays": {
"title": "Les jours complémentaires",
"sections": [
{
"heading": "Le solde de l'année",
"paragraphs": [
"Douze mois de trente jours ne totalisent que 360 jours : il manque cinq jours pour compléter une année tropique, six les années sextiles. Ces jours, d'abord appelés « épagomènes » puis « sans-culottides » par le décret du 4 frimaire an II, prennent le nom de « jours complémentaires » à partir du décret du 7 fructidor an III (24 août 1795).",
"Placés hors des mois et des décades, ils ne portent pas de nom de décadi et sont consacrés aux réjouissances et fêtes publiques marquant la fin de l'année républicaine, autour du 17 au 22 septembre du calendrier grégorien."
]
},
{
"heading": "Les cinq (ou six) journées",
"paragraphs": [
"Le jour de la Vertu, le jour du Génie, le jour du Travail, le jour de l'Opinion et le jour des Récompenses composent les cinq jours complémentaires de toute année républicaine.",
"Les années sextiles — tous les quatre ans dans la règle perpétuelle de Romme — s'y ajoute un sixième jour, le jour de la Révolution, consacré aux jeux nationaux en mémoire de la fondation de la République.",
"La période de quatre ans séparant deux années sextiles porte le nom de « Franciade », en écho aux Olympiades de la Grèce antique."
]
}
]
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"site": {
"title": "Calendrier républicain",
"tagline": "Convertisseur perpétuel"
},
"nav": {
"menuLabel": "Menu",
"home": "Accueil",
"about": "À propos",
"aboutHistory": "Histoire du calendrier",
"aboutLeapYears": "Les années sextiles",
"aboutComplementaryDays": "Les jours complémentaires",
"language": "Langue"
},
"home": {
"todayHeading": "Nous sommes aujourd'hui le",
"resultHeading": "Date convertie",
"gregorianEquivalent": "Équivalent grégorien",
"sextileBadge": "Année sextile",
"decadeLabel": "Décade",
"dayOfYearLabel": "Quantième de l'année",
"yearLabel": "An"
},
"form": {
"title": "Convertir une autre date",
"dateLabel": "Date grégorienne (JJ/MM/AAAA)",
"submit": "Convertir",
"todayButton": "Revenir à aujourd'hui",
"helpText": "Sélectionnez un jour, un mois et une année : le formulaire vérifie automatiquement que la date existe.",
"errorInvalid": "Cette date n'existe pas dans le calendrier grégorien.",
"errorBeforeEpoch": "Le calendrier républicain débute le 22 septembre 1792 (1er vendémiaire an I). Choisissez une date postérieure.",
"errorGeneric": "Une erreur est survenue pendant la conversion."
},
"weekdays": {
"monday": "Lundi",
"tuesday": "Mardi",
"wednesday": "Mercredi",
"thursday": "Jeudi",
"friday": "Vendredi",
"saturday": "Samedi",
"sunday": "Dimanche"
},
"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).",
"sourceNote": "Contenu historique inspiré de l'article « Calendrier républicain »."
},
"loading": "Chargement…"
}
+135
View File
@@ -0,0 +1,135 @@
{
"seasonSuffix": {
"autumn": "Terminaison en « -aire » (du latin -arius)",
"winter": "Terminaison en « -ôse » (du latin -osus, « doté de »)",
"spring": "Terminaison en « -al » (du latin -alis)",
"summer": "Terminaison en « -idor » (du grec dôron, « don »)"
},
"months": {
"vendemiaire": {
"name": "Vendémiaire",
"season": "autumn",
"period": "22/23/24 septembre 21/22/23 octobre",
"etymology": "Du latin vindemia, « vendange ».",
"description": "Premier mois de l'année républicaine, il ouvre l'automne et débute le jour de l'équinoxe. Fabre d'Églantine y voit le mois du raisin et des vendanges qui achèvent le travail de la vigne."
},
"brumaire": {
"name": "Brumaire",
"season": "autumn",
"period": "22/23/24 octobre 20/21/22 novembre",
"etymology": "Du latin bruma, « brouillard, brume ».",
"description": "Mois des brumes et des brouillards qui enveloppent la campagne, correspondant à la transsudation de la nature entre octobre et novembre."
},
"frimaire": {
"name": "Frimaire",
"season": "autumn",
"period": "21/22/23 novembre 20/21/22 décembre",
"etymology": "Du mot « frimas », froid humide de la fin d'automne.",
"description": "Dernier mois de l'automne, marqué par les premiers froids, tantôt secs, tantôt humides, qui annoncent l'hiver."
},
"nivose": {
"name": "Nivôse",
"season": "winter",
"period": "21/22/23 décembre 19/20/21 janvier",
"etymology": "Du latin nix, nivis, « neige ».",
"description": "Premier mois de l'hiver, celui de la neige qui blanchit la terre entre décembre et janvier."
},
"pluviose": {
"name": "Pluviôse",
"season": "winter",
"period": "20/21/22 janvier 18/19/20 février",
"etymology": "Du latin pluvia, « pluie ».",
"description": "Mois des pluies abondantes qui tombent généralement entre janvier et février."
},
"ventose": {
"name": "Ventôse",
"season": "winter",
"period": "19/20/21 février 20/21 mars",
"etymology": "Du latin ventosus, « venteux ».",
"description": "Dernier mois de l'hiver, celui des giboulées et des vents qui sèchent la terre entre février et mars."
},
"germinal": {
"name": "Germinal",
"season": "spring",
"period": "21/22 mars 19/20 avril",
"etymology": "Du latin germen, « germe ».",
"description": "Premier mois du printemps, celui de la fermentation et du développement de la sève entre mars et avril."
},
"floreal": {
"name": "Floréal",
"season": "spring",
"period": "20/21 avril 19/20 mai",
"etymology": "Du latin flos, floris, « fleur ».",
"description": "Mois de l'épanouissement des fleurs entre avril et mai."
},
"prairial": {
"name": "Prairial",
"season": "spring",
"period": "20/21 mai 18/19 juin",
"etymology": "Du mot « prairie ».",
"description": "Dernier mois du printemps, celui de la fécondité riante et de la récolte des prairies entre mai et juin."
},
"messidor": {
"name": "Messidor",
"season": "summer",
"period": "19/20 juin 18/19 juillet",
"etymology": "Du latin messis, « moisson », et du grec dôron, « don ».",
"description": "Premier mois de l'été, celui des épis ondoyants et des moissons dorées entre juin et juillet."
},
"thermidor": {
"name": "Thermidor",
"season": "summer",
"period": "19/20 juillet 17/18 août",
"etymology": "Du grec thermos, « chaleur », et dôron, « don ».",
"description": "Mois de la chaleur, à la fois solaire et terrestre, qui embrase l'air entre juillet et août."
},
"fructidor": {
"name": "Fructidor",
"season": "summer",
"period": "18/19 août 16/17 septembre",
"etymology": "Du latin fructus, « fruit », et du grec dôron, « don ».",
"description": "Dernier mois de l'année, celui des fruits que le soleil dore et mûrit entre août et septembre."
}
},
"decadeDays": {
"primidi": "Primidi",
"duodi": "Duodi",
"tridi": "Tridi",
"quartidi": "Quartidi",
"quintidi": "Quintidi",
"sextidi": "Sextidi",
"septidi": "Septidi",
"octidi": "Octidi",
"nonidi": "Nonidi",
"decadi": "Décadi"
},
"complementaryDays": {
"title": "Jours complémentaires (sans-culottides)",
"days": {
"virtue": {
"name": "Jour de la Vertu",
"description": "Premier jour complémentaire, dédié à la vertu civique."
},
"genius": {
"name": "Jour du Génie",
"description": "Deuxième jour complémentaire, célébrant le génie et l'esprit d'invention."
},
"labour": {
"name": "Jour du Travail",
"description": "Troisième jour complémentaire, en l'honneur du travail."
},
"opinion": {
"name": "Jour de l'Opinion",
"description": "Quatrième jour complémentaire, consacré à la liberté d'opinion."
},
"rewards": {
"name": "Jour des Récompenses",
"description": "Cinquième jour complémentaire, jour des récompenses publiques."
},
"revolution": {
"name": "Jour de la Révolution",
"description": "Sixième jour complémentaire, ajouté uniquement les années sextiles, en mémoire de la Révolution et des jeux nationaux."
}
}
}
}
+832
View File
@@ -0,0 +1,832 @@
{
"name": "calendrier-republicain",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "calendrier-republicain",
"version": "1.0.0",
"dependencies": {
"express": "^4.19.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.16.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
"integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.13",
"proxy-addr": "~2.0.7",
"qs": "~6.16.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "calendrier-republicain",
"version": "1.0.0",
"description": "Convertisseur perpetuel du calendrier gregorien vers le calendrier republicain francais",
"private": true,
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
+9
View File
@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 483 B

+9
View File
@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 554 B

+14
View File
@@ -0,0 +1,14 @@
<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>

After

Width:  |  Height:  |  Size: 956 B

+13
View File
@@ -0,0 +1,13 @@
<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>

After

Width:  |  Height:  |  Size: 611 B

+8
View File
@@ -0,0 +1,8 @@
<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>

After

Width:  |  Height:  |  Size: 714 B

+7
View File
@@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 614 B

+16
View File
@@ -0,0 +1,16 @@
<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>

After

Width:  |  Height:  |  Size: 702 B

+14
View File
@@ -0,0 +1,14 @@
<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>

After

Width:  |  Height:  |  Size: 659 B

+10
View File
@@ -0,0 +1,10 @@
<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>

After

Width:  |  Height:  |  Size: 500 B

+15
View File
@@ -0,0 +1,15 @@
<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>

After

Width:  |  Height:  |  Size: 649 B

+14
View File
@@ -0,0 +1,14 @@
<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>

After

Width:  |  Height:  |  Size: 694 B

+16
View File
@@ -0,0 +1,16 @@
<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>

After

Width:  |  Height:  |  Size: 820 B

+8
View File
@@ -0,0 +1,8 @@
<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>

After

Width:  |  Height:  |  Size: 395 B

+446
View File
@@ -0,0 +1,446 @@
:root {
--cream-bg: #faf3e6;
--cream-panel: #f6efe0;
--cream-panel-alt: #efe4cc;
--ink: #3a2e22;
--ink-soft: #6b5a45;
--accent: #b5652b;
--accent-soft: #d99a5b;
--brass: #8a6b45;
--border: #d8c9a8;
--shadow: 0 6px 20px rgba(90, 66, 34, 0.12);
color-scheme: light;
}
* {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
html, body {
margin: 0;
padding: 0;
}
body {
background: var(--cream-bg);
background-image: radial-gradient(circle at 10% 10%, rgba(181, 101, 43, 0.05), transparent 40%),
radial-gradient(circle at 90% 20%, rgba(138, 107, 69, 0.06), transparent 45%);
color: var(--ink);
font-family: 'Inter', 'Segoe UI', sans-serif;
line-height: 1.55;
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1, h2, .brand-title, .republican-date, .republican-year {
font-family: 'Fraunces', 'Georgia', serif;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
/* Header */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
background: var(--cream-panel);
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
color: var(--ink);
}
.brand-mark {
font-family: 'Fraunces', serif;
font-style: italic;
font-size: 0.9rem;
color: var(--accent);
border: 1px solid var(--accent);
border-radius: 999px;
padding: 0.1rem 0.6rem;
}
.brand-title {
font-size: 1.15rem;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.lang-switch {
border: 1px solid var(--brass);
background: transparent;
color: var(--ink);
border-radius: 999px;
padding: 0.35rem 0.75rem;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.lang-switch:hover {
background: var(--cream-panel-alt);
}
.menu-toggle {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 40px;
height: 36px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--cream-bg);
cursor: pointer;
padding: 0 8px;
}
.burger-bar {
display: block;
height: 2px;
background: var(--ink);
border-radius: 2px;
}
/* Nav panel */
.main-nav {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(300px, 85vw);
background: var(--cream-panel);
border-left: 1px solid var(--border);
box-shadow: var(--shadow);
padding: calc(1.5rem + env(safe-area-inset-top, 0px)) 1.5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
transform: translateX(100%);
transition: transform 0.25s ease;
z-index: 30;
}
.main-nav.open {
transform: translateX(0);
}
.nav-link {
color: var(--ink);
text-decoration: none;
padding: 0.6rem 0.5rem;
border-radius: 8px;
font-weight: 500;
}
.nav-link:hover, .nav-link.active {
background: var(--cream-panel-alt);
color: var(--accent);
}
.nav-section-label {
margin: 1rem 0 0.1rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-soft);
}
.nav-backdrop {
position: fixed;
inset: 0;
background: rgba(58, 46, 34, 0.35);
z-index: 25;
}
/* Main content */
main#app {
flex: 1;
width: 100%;
max-width: 1080px;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
}
.today-heading {
text-align: center;
font-weight: 400;
font-size: 1.4rem;
color: var(--ink-soft);
margin-bottom: 1.5rem;
}
.calendar-card {
display: grid;
grid-template-columns: minmax(180px, 220px) 1fr minmax(220px, 320px);
gap: 1.5rem;
align-items: stretch;
background: var(--cream-panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
box-shadow: var(--shadow);
}
.month-illustration {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
text-align: center;
border-right: 1px dashed var(--border);
padding-right: 1.5rem;
}
.month-illustration img {
width: 100%;
max-width: 150px;
height: auto;
}
.month-period {
font-size: 0.85rem;
color: var(--ink-soft);
font-style: italic;
}
.date-display {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 0.3rem;
}
.decade-day {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.85rem;
color: var(--accent);
font-weight: 600;
margin: 0;
}
.republican-date {
font-size: clamp(1.8rem, 4vw, 2.6rem);
margin: 0.1rem 0;
font-weight: 600;
}
.republican-year {
font-size: 1.1rem;
color: var(--ink-soft);
margin: 0;
}
.sextile-badge {
display: inline-block;
margin-top: 0.5rem;
background: var(--accent);
color: var(--cream-bg);
border-radius: 999px;
padding: 0.2rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
}
.gregorian-equivalent {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--ink-soft);
}
.description-panel {
border-left: 1px dashed var(--border);
padding-left: 1.5rem;
}
.month-name {
margin: 0 0 0.2rem;
font-size: 1.3rem;
}
.month-season-suffix {
font-size: 0.8rem;
color: var(--brass);
margin: 0 0 0.6rem;
}
.month-etymology {
font-style: italic;
color: var(--ink-soft);
margin: 0 0 0.6rem;
}
.month-description {
margin: 0;
}
/* Form */
.convert-form {
margin-top: 2rem;
background: var(--cream-panel-alt);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
}
.convert-form h2 {
margin-top: 0;
font-size: 1.1rem;
}
.convert-form label {
display: block;
font-size: 0.85rem;
margin-bottom: 0.4rem;
color: var(--ink-soft);
}
.convert-form-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.convert-form-row input[type="date"] {
font-family: inherit;
font-size: 1rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--cream-bg);
color: var(--ink);
}
.convert-form-row button {
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
padding: 0.55rem 1.1rem;
border-radius: 8px;
border: 1px solid var(--accent);
background: var(--accent);
color: #fff;
cursor: pointer;
}
.convert-form-row button.secondary {
background: transparent;
color: var(--accent);
}
.convert-form-row button:hover {
filter: brightness(1.05);
}
.form-help {
margin: 0.75rem 0 0;
font-size: 0.8rem;
color: var(--ink-soft);
}
.form-error {
margin: 0.75rem 0 0;
font-size: 0.85rem;
color: #a13f2b;
font-weight: 600;
}
/* About view */
.about-view {
background: var(--cream-panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
box-shadow: var(--shadow);
}
.about-view h1 {
margin-top: 0;
}
.about-content h2 {
font-size: 1.15rem;
color: var(--accent);
margin: 1.5rem 0 0.5rem;
}
.about-content h2:first-child {
margin-top: 0;
}
.about-content p {
margin: 0 0 0.75rem;
}
/* Footer */
.site-footer {
text-align: center;
padding: 1.5rem 1rem calc(1.5rem + env(safe-area-inset-bottom, 0px));
font-size: 0.8rem;
color: var(--ink-soft);
border-top: 1px solid var(--border);
}
/* Responsive */
@media (max-width: 820px) {
.calendar-card {
grid-template-columns: 1fr;
text-align: center;
}
.month-illustration,
.description-panel {
border: none;
padding: 0;
}
.month-illustration {
padding-bottom: 1rem;
border-bottom: 1px dashed var(--border);
}
.description-panel {
padding-top: 1rem;
border-top: 1px dashed var(--border);
}
}
@media (max-width: 480px) {
.site-header {
padding: 0.75rem 1rem;
}
main#app {
padding: 1.5rem 0.9rem 2rem;
}
.calendar-card, .convert-form, .about-view {
padding: 1.25rem;
}
}
+96
View File
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="fr">
<head>
<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="stylesheet" href="css/style.css">
</head>
<body>
<header class="site-header">
<a class="brand" href="#/">
<span class="brand-mark">an</span>
<span class="brand-title" data-i18n="site.title">Calendrier républicain</span>
</a>
<div class="header-actions">
<button id="lang-switch" class="lang-switch" type="button" aria-label="Switch language">FR</button>
<button id="menu-toggle" class="menu-toggle" type="button" aria-expanded="false" aria-controls="main-nav">
<span class="sr-only" data-i18n="nav.menuLabel">Menu</span>
<span class="burger-bar"></span>
<span class="burger-bar"></span>
<span class="burger-bar"></span>
</button>
</div>
</header>
<nav id="main-nav" class="main-nav" aria-hidden="true">
<a href="#/" class="nav-link" data-view="home" data-i18n="nav.home">Accueil</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>
<a href="#/about/complementary-days" class="nav-link" data-view="complementary-days" data-i18n="nav.aboutComplementaryDays">Les jours complémentaires</a>
</nav>
<div id="nav-backdrop" class="nav-backdrop" hidden></div>
<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="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>
<div class="date-display">
<p id="decade-day" class="decade-day"></p>
<p id="republican-date" class="republican-date"></p>
<p id="republican-year" class="republican-year"></p>
<span id="sextile-badge" class="sextile-badge" hidden data-i18n="home.sextileBadge">Année sextile</span>
<p class="gregorian-equivalent">
<span data-i18n="home.gregorianEquivalent">Équivalent grégorien</span> :
<span id="gregorian-equivalent-value"></span>
</p>
</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>
<form id="convert-form" class="convert-form">
<h2 data-i18n="form.title">Convertir une autre date</h2>
<label for="date-input" data-i18n="form.dateLabel">Date grégorienne (JJ/MM/AAAA)</label>
<div class="convert-form-row">
<input type="date" id="date-input" name="date" min="1792-09-22" required>
<button type="submit" data-i18n="form.submit">Convertir</button>
<button type="button" id="today-button" class="secondary" data-i18n="form.todayButton">Revenir à aujourd'hui</button>
</div>
<p class="form-help" data-i18n="form.helpText">Sélectionnez un jour, un mois et une année : le formulaire vérifie automatiquement que la date existe.</p>
<p id="form-error" class="form-error" hidden></p>
</form>
</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>
<footer class="site-footer">
<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>
<script src="js/i18n.js"></script>
<script src="js/app.js"></script>
</body>
</html>
+230
View File
@@ -0,0 +1,230 @@
'use strict';
(() => {
const monthImage = document.getElementById('month-image');
const monthPeriod = document.getElementById('month-period');
const decadeDay = document.getElementById('decade-day');
const republicanDate = document.getElementById('republican-date');
const republicanYear = document.getElementById('republican-year');
const sextileBadge = document.getElementById('sextile-badge');
const gregorianEquivalentValue = document.getElementById('gregorian-equivalent-value');
const monthName = document.getElementById('month-name');
const monthSeasonSuffix = document.getElementById('month-season-suffix');
const monthEtymology = document.getElementById('month-etymology');
const monthDescription = document.getElementById('month-description');
const dateInput = document.getElementById('date-input');
const convertForm = document.getElementById('convert-form');
const todayButton = document.getElementById('today-button');
const formError = document.getElementById('form-error');
const viewHome = document.getElementById('view-home');
const viewAbout = document.getElementById('view-about');
const aboutTitle = document.getElementById('about-title');
const aboutContent = document.getElementById('about-content');
const menuToggle = document.getElementById('menu-toggle');
const mainNav = document.getElementById('main-nav');
const navBackdrop = document.getElementById('nav-backdrop');
const langSwitch = document.getElementById('lang-switch');
const ABOUT_ROUTES = {
'about/history': 'history',
'about/leap-years': 'leapYears',
'about/complementary-days': 'complementaryDays',
};
let lastRepublicanResult = null;
function pad(value) {
return String(value).padStart(2, '0');
}
function toIsoDate({ year, month, day }) {
return `${String(year).padStart(4, '0')}-${pad(month)}-${pad(day)}`;
}
function formatGregorian({ year, month, day }, weekdayKey) {
const weekdayLabel = I18N.t(`weekdays.${weekdayKey}`, '');
return `${weekdayLabel} ${pad(day)}/${pad(month)}/${year}`.trim();
}
function closeMenu() {
mainNav.classList.remove('open');
mainNav.setAttribute('aria-hidden', 'true');
navBackdrop.hidden = true;
menuToggle.setAttribute('aria-expanded', 'false');
}
function openMenu() {
mainNav.classList.add('open');
mainNav.setAttribute('aria-hidden', 'false');
navBackdrop.hidden = false;
menuToggle.setAttribute('aria-expanded', 'true');
}
function renderRepublicanResult(result) {
lastRepublicanResult = result;
const months = I18N.months.months || {};
const decadeDays = I18N.months.decadeDays || {};
const complementary = I18N.months.complementaryDays || { days: {} };
sextileBadge.hidden = !result.isSextile;
gregorianEquivalentValue.textContent = formatGregorian(result.gregorianDate, result.gregorianWeekdayKey);
republicanYear.textContent = `${I18N.t('home.yearLabel')} ${result.republicanYearRoman} (${result.republicanYear})`;
if (result.isComplementaryDay) {
const day = complementary.days[result.complementaryDayKey] || {};
decadeDay.textContent = complementary.title || '';
republicanDate.textContent = day.name || '';
monthImage.src = 'assets/months/complementary.svg';
monthImage.alt = day.name || '';
monthPeriod.textContent = '';
monthName.textContent = day.name || '';
monthSeasonSuffix.textContent = '';
monthEtymology.textContent = '';
monthDescription.textContent = day.description || '';
} else {
const month = months[result.monthKey] || {};
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.alt = month.name || '';
monthPeriod.textContent = month.period || '';
monthName.textContent = month.name || '';
monthSeasonSuffix.textContent = seasonSuffix;
monthEtymology.textContent = month.etymology || '';
monthDescription.textContent = month.description || '';
}
}
function showFormError(messageKey) {
formError.textContent = I18N.t(messageKey);
formError.hidden = false;
}
function hideFormError() {
formError.hidden = true;
formError.textContent = '';
}
async function fetchAndRender(url) {
hideFormError();
const response = await fetch(url);
const payload = await response.json();
if (!payload.ok) {
if (payload.error === 'BEFORE_EPOCH') {
showFormError('form.errorBeforeEpoch');
} else if (payload.error === 'INVALID_DATE') {
showFormError('form.errorInvalid');
} else {
showFormError('form.errorGeneric');
}
return;
}
renderRepublicanResult(payload.republicanDate);
dateInput.value = toIsoDate(payload.republicanDate.gregorianDate);
}
async function loadToday() {
try {
await fetchAndRender('/api/today');
} catch (error) {
showFormError('form.errorGeneric');
}
}
async function handleConvertSubmit(event) {
event.preventDefault();
const value = dateInput.value; // yyyy-mm-dd from <input type="date">
if (!value) return;
const [year, month, day] = value.split('-').map((part) => Number.parseInt(part, 10));
try {
await fetchAndRender(`/api/convert?year=${year}&month=${month}&day=${day}`);
} catch (error) {
showFormError('form.errorGeneric');
}
}
function renderAboutView(sectionKey) {
const section = I18N.about[sectionKey];
if (!section) return;
aboutTitle.textContent = section.title;
aboutContent.innerHTML = '';
(section.sections || []).forEach((block) => {
const heading = document.createElement('h2');
heading.textContent = block.heading;
aboutContent.appendChild(heading);
(block.paragraphs || []).forEach((paragraph) => {
const p = document.createElement('p');
p.textContent = paragraph;
aboutContent.appendChild(p);
});
});
}
function route() {
const hash = window.location.hash.replace(/^#\/?/, '');
closeMenu();
document.querySelectorAll('.nav-link').forEach((link) => link.classList.remove('active'));
if (hash in ABOUT_ROUTES) {
viewHome.hidden = true;
viewAbout.hidden = false;
renderAboutView(ABOUT_ROUTES[hash]);
const activeLink = document.querySelector(`.nav-link[href="#/${hash}"]`);
if (activeLink) activeLink.classList.add('active');
} else {
viewHome.hidden = false;
viewAbout.hidden = true;
const activeLink = document.querySelector('.nav-link[href="#/"]');
if (activeLink) activeLink.classList.add('active');
}
}
function bindEvents() {
menuToggle.addEventListener('click', () => {
if (mainNav.classList.contains('open')) {
closeMenu();
} else {
openMenu();
}
});
navBackdrop.addEventListener('click', closeMenu);
window.addEventListener('hashchange', route);
convertForm.addEventListener('submit', handleConvertSubmit);
todayButton.addEventListener('click', () => {
loadToday();
});
langSwitch.addEventListener('click', async () => {
const nextLang = I18N.currentLang === 'fr' ? 'en' : 'fr';
await I18N.setLang(nextLang);
});
I18N.onChange((lang) => {
langSwitch.textContent = lang.toUpperCase();
if (lastRepublicanResult) {
renderRepublicanResult(lastRepublicanResult);
}
if (!viewAbout.hidden) {
const hash = window.location.hash.replace(/^#\/?/, '');
if (hash in ABOUT_ROUTES) renderAboutView(ABOUT_ROUTES[hash]);
}
});
}
async function init() {
bindEvents();
const initialLang = I18N.detectInitialLang();
await I18N.load(initialLang);
langSwitch.textContent = initialLang.toUpperCase();
route();
await loadToday();
}
init();
})();
+93
View File
@@ -0,0 +1,93 @@
'use strict';
const I18N = (() => {
const SUPPORTED_LANGS = ['fr', 'en'];
const DEFAULT_LANG = 'fr';
const STORAGE_KEY = 'republican-calendar-lang';
let currentLang = DEFAULT_LANG;
let common = {};
let months = {};
let about = {};
const changeListeners = [];
function detectInitialLang() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && SUPPORTED_LANGS.includes(stored)) return stored;
} catch (error) {
// localStorage unavailable (private mode, etc.): fall through to default.
}
return DEFAULT_LANG;
}
function getByPath(object, path) {
return path.split('.').reduce((node, key) => (node && node[key] !== undefined ? node[key] : undefined), object);
}
function t(path, fallback) {
const value = getByPath(common, path);
return value !== undefined ? value : (fallback !== undefined ? fallback : path);
}
function applyStaticTranslations() {
document.documentElement.lang = currentLang;
document.querySelectorAll('[data-i18n]').forEach((element) => {
const key = element.getAttribute('data-i18n');
const value = getByPath(common, key);
if (value !== undefined) {
element.textContent = value;
}
});
}
async function fetchJson(url) {
const response = await fetch(url, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Failed to load ${url}: ${response.status}`);
}
return response.json();
}
async function load(lang) {
const targetLang = SUPPORTED_LANGS.includes(lang) ? lang : DEFAULT_LANG;
const [commonData, monthsData, aboutData] = await Promise.all([
fetchJson(`locales/${targetLang}/common.json`),
fetchJson(`locales/${targetLang}/months.json`),
fetchJson(`locales/${targetLang}/about.json`),
]);
currentLang = targetLang;
common = commonData;
months = monthsData;
about = aboutData;
applyStaticTranslations();
changeListeners.forEach((listener) => listener(currentLang));
}
async function setLang(lang) {
if (!SUPPORTED_LANGS.includes(lang) || lang === currentLang) return;
try {
localStorage.setItem(STORAGE_KEY, lang);
} catch (error) {
// Ignore storage failures; language just won't persist across reloads.
}
await load(lang);
}
function onChange(listener) {
changeListeners.push(listener);
}
return {
SUPPORTED_LANGS,
get currentLang() { return currentLang; },
get common() { return common; },
get months() { return months; },
get about() { return about; },
detectInitialLang,
load,
setLang,
onChange,
t,
};
})();
+66
View File
@@ -0,0 +1,66 @@
'use strict';
const path = require('path');
const express = require('express');
const { gregorianToRepublican } = require('./src/republicanCalendar');
const app = express();
const port = process.env.PORT || 3000;
app.use(express.static(path.join(__dirname, 'public')));
app.use('/locales', express.static(path.join(__dirname, 'locales')));
function nowUtc() {
const now = new Date();
return {
year: now.getUTCFullYear(),
month: now.getUTCMonth() + 1,
day: now.getUTCDate(),
};
}
function convertAndRespond(res, gregorianDate) {
try {
const republicanDate = gregorianToRepublican(gregorianDate);
res.json({ ok: true, republicanDate });
} catch (error) {
if (error.message === 'BEFORE_EPOCH') {
res.status(400).json({
ok: false,
error: 'BEFORE_EPOCH',
message: 'The Republican calendar begins on 22 September 1792.',
});
return;
}
res.status(400).json({
ok: false,
error: 'INVALID_DATE',
message: 'The given date does not exist in the Gregorian calendar.',
});
}
}
app.get('/api/today', (req, res) => {
convertAndRespond(res, nowUtc());
});
app.get('/api/convert', (req, res) => {
const year = Number.parseInt(req.query.year, 10);
const month = Number.parseInt(req.query.month, 10);
const day = Number.parseInt(req.query.day, 10);
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
res.status(400).json({
ok: false,
error: 'INVALID_DATE',
message: 'year, month and day query parameters are required integers.',
});
return;
}
convertAndRespond(res, { year, month, day });
});
app.listen(port, () => {
console.log(`Calendrier republicain server listening on port ${port}`);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

+226
View File
@@ -0,0 +1,226 @@
'use strict';
/**
* French Republican Calendar conversion.
*
* Uses the perpetual arithmetic rule proposed by Gilbert Romme on
* 19 floréal an III (8 May 1795) to make the calendar predictable:
* a sextile (leap) year occurs every 4 years, except centennial years
* unless divisible by 400, except the 40th centennial cycle (year 4000
* and its multiples), which stays common. This mirrors the Gregorian
* rule but applied to the Republican year number.
*
* Epoch: 1 Vendémiaire an I = 22 September 1792 (proclamation of the
* Republic / autumnal equinox at the Paris meridian).
*/
const EPOCH_GREGORIAN = { year: 1792, month: 9, day: 22 };
const MONTH_KEYS = [
'vendemiaire',
'brumaire',
'frimaire',
'nivose',
'pluviose',
'ventose',
'germinal',
'floreal',
'prairial',
'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',
];
const ROMAN_NUMERAL_MAP = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'],
];
function toRomanNumeral(number) {
if (!Number.isInteger(number) || number <= 0) {
return String(number);
}
let remaining = number;
let result = '';
for (const [value, symbol] of ROMAN_NUMERAL_MAP) {
while (remaining >= value) {
result += symbol;
remaining -= value;
}
}
return result;
}
/** Proleptic Gregorian calendar date to Julian Day Number. */
function gregorianToJulianDayNumber(year, month, day) {
const a = Math.floor((14 - month) / 12);
const y = year + 4800 - a;
const m = month + 12 * a - 3;
return (
day +
Math.floor((153 * m + 2) / 5) +
365 * y +
Math.floor(y / 4) -
Math.floor(y / 100) +
Math.floor(y / 400) -
32045
);
}
function isSextileYear(republicanYear) {
if (republicanYear % 4 !== 0) return false;
if (republicanYear % 4000 === 0) return false;
if (republicanYear % 100 === 0 && republicanYear % 400 !== 0) return false;
return true;
}
function republicanYearLength(republicanYear) {
return isSextileYear(republicanYear) ? 366 : 365;
}
const EPOCH_JDN = gregorianToJulianDayNumber(
EPOCH_GREGORIAN.year,
EPOCH_GREGORIAN.month,
EPOCH_GREGORIAN.day
);
/**
* Converts a Gregorian calendar date (proleptic, UTC) into its Republican
* calendar equivalent.
*
* @param {{year: number, month: number, day: number}} gregorianDate month is 1-12
* @returns {object} Republican date breakdown
*/
function gregorianToRepublican({ year, month, day }) {
if (
!Number.isInteger(year) ||
!Number.isInteger(month) ||
!Number.isInteger(day) ||
month < 1 ||
month > 12 ||
day < 1 ||
day > 31
) {
throw new RangeError('INVALID_DATE');
}
const jdn = gregorianToJulianDayNumber(year, month, day);
// Round-trip check to reject impossible calendar dates (e.g. 31 April).
const checkYear = year;
const checkMonth = month;
const checkDay = day;
const reconstructed = julianDayNumberToGregorian(jdn);
if (
reconstructed.year !== checkYear ||
reconstructed.month !== checkMonth ||
reconstructed.day !== checkDay
) {
throw new RangeError('INVALID_DATE');
}
if (jdn < EPOCH_JDN) {
throw new RangeError('BEFORE_EPOCH');
}
let remaining = jdn - EPOCH_JDN;
let republicanYear = 1;
while (remaining >= republicanYearLength(republicanYear)) {
remaining -= republicanYearLength(republicanYear);
republicanYear += 1;
}
const dayOfYear = remaining; // 0-indexed
const sextile = isSextileYear(republicanYear);
const result = {
republicanYear,
republicanYearRoman: toRomanNumeral(republicanYear),
isSextile: sextile,
dayOfYear: dayOfYear + 1,
gregorianDate: { year, month, day },
gregorianWeekdayKey: weekdayKeyFromJdn(jdn),
};
if (dayOfYear < 360) {
const monthIndex = Math.floor(dayOfYear / 30); // 0-11
const dayOfMonth = (dayOfYear % 30) + 1; // 1-30
const decadeDayIndex = (dayOfMonth - 1) % 10; // 0-9
result.isComplementaryDay = false;
result.monthIndex = monthIndex + 1; // 1-12
result.monthKey = MONTH_KEYS[monthIndex];
result.dayOfMonth = dayOfMonth;
result.decadeIndex = Math.floor((dayOfMonth - 1) / 10) + 1; // 1-3
result.decadeDayIndex = decadeDayIndex + 1; // 1-10
result.decadeDayKey = DECADE_DAY_KEYS[decadeDayIndex];
} else {
const complementaryIndex = dayOfYear - 360; // 0-4 or 0-5
result.isComplementaryDay = true;
result.monthIndex = null;
result.monthKey = null;
result.dayOfMonth = null;
result.decadeIndex = null;
result.decadeDayIndex = null;
result.decadeDayKey = null;
result.complementaryDayIndex = complementaryIndex + 1; // 1-6
result.complementaryDayKey = COMPLEMENTARY_DAY_KEYS[complementaryIndex];
}
return result;
}
function weekdayKeyFromJdn(jdn) {
// JDN 0 (1 Jan 4713 BCE proleptic) is a Monday.
const index = ((jdn % 7) + 7) % 7;
const MONDAY_BASED = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
return MONDAY_BASED[index];
}
/** Julian Day Number to proleptic Gregorian calendar date. */
function julianDayNumberToGregorian(jdn) {
const a = jdn + 32044;
const b = Math.floor((4 * a + 3) / 146097);
const c = a - Math.floor((146097 * b) / 4);
const d = Math.floor((4 * c + 3) / 1461);
const e = c - Math.floor((1461 * d) / 4);
const m = Math.floor((5 * e + 2) / 153);
const day = e - Math.floor((153 * m + 2) / 5) + 1;
const month = m + 3 - 12 * Math.floor(m / 10);
const year = 100 * b + d - 4800 + Math.floor(m / 10);
return { year, month, day };
}
module.exports = {
EPOCH_GREGORIAN,
MONTH_KEYS,
DECADE_DAY_KEYS,
COMPLEMENTARY_DAY_KEYS,
toRomanNumeral,
isSextileYear,
republicanYearLength,
gregorianToRepublican,
gregorianToJulianDayNumber,
};