refacto: Remake the interface so it is more 'vintage' looking + Adding a full calendar page
This commit is contained in:
+223
-9
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user