feat: Building the app in it's first version
This commit is contained in:
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user