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
+223 -9
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;
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();