- Public site (Home/Artists/Exhibitions/News/About/Contact) with EN/KO/JA i18n - Admin panel with login, CRUD, image upload, multilingual editing - Exhibition slider/lightbox view - FastAPI + MongoDB backend, JWT auth - Docker Compose deployment, behind nginx at jimi.yakenator.io
130 lines
3.9 KiB
JavaScript
130 lines
3.9 KiB
JavaScript
// Shared helpers for public pages.
|
||
|
||
function fmtDate(iso) {
|
||
if (!iso) return "";
|
||
const d = new Date(iso + "T00:00:00");
|
||
return d.toLocaleDateString(getLocale(), {
|
||
month: "long",
|
||
day: "numeric",
|
||
year: "numeric"
|
||
});
|
||
}
|
||
|
||
function fmtRange(start, end) {
|
||
if (!start || !end) return "";
|
||
const locale = getLocale();
|
||
const s = new Date(start + "T00:00:00");
|
||
const e = new Date(end + "T00:00:00");
|
||
const sMonth = s.toLocaleDateString(locale, { month: "long" });
|
||
const eMonth = e.toLocaleDateString(locale, { month: "long" });
|
||
const sameYear = s.getFullYear() === e.getFullYear();
|
||
if (sMonth === eMonth && sameYear) {
|
||
return `${sMonth} ${s.getDate()} – ${e.getDate()}, ${e.getFullYear()}`;
|
||
}
|
||
if (sameYear) {
|
||
return `${sMonth} ${s.getDate()} – ${eMonth} ${e.getDate()}, ${e.getFullYear()}`;
|
||
}
|
||
return `${sMonth} ${s.getDate()}, ${s.getFullYear()} – ${eMonth} ${e.getDate()}, ${e.getFullYear()}`;
|
||
}
|
||
|
||
function qs(key) {
|
||
return new URLSearchParams(location.search).get(key);
|
||
}
|
||
|
||
function mount(id, html) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.innerHTML = html;
|
||
}
|
||
|
||
const LOGO_SVG = `
|
||
<svg class="brand-mark" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||
<circle cx="50" cy="50" r="50" fill="#6b3d8f"/>
|
||
<g fill="#ffffff" font-family="'Cormorant Garamond','Playfair Display','Times New Roman',serif" font-weight="500" font-style="italic">
|
||
<text x="37" y="70" text-anchor="middle" font-size="60">J</text>
|
||
<text x="63" y="70" text-anchor="middle" font-size="60">M</text>
|
||
</g>
|
||
</svg>
|
||
`;
|
||
|
||
function navHtml(active) {
|
||
const items = [
|
||
["index.html", t("nav.home"), "home"],
|
||
["artists.html", t("nav.artists"), "artists"],
|
||
["exhibitions.html", t("nav.exhibitions"), "exhibitions"],
|
||
["news.html", t("nav.news"), "news"],
|
||
["about.html", t("nav.about"), "about"],
|
||
["contact.html", t("nav.contact"), "contact"]
|
||
];
|
||
const s = Store.settings();
|
||
return `
|
||
<nav class="nav">
|
||
<div class="nav-inner">
|
||
<a href="index.html" class="brand">
|
||
${LOGO_SVG}
|
||
<span class="brand-word">${s.galleryName}</span>
|
||
</a>
|
||
<div class="nav-right">
|
||
<div class="nav-links">
|
||
${items
|
||
.map(
|
||
([href, label, key]) =>
|
||
`<a href="${href}" class="${
|
||
active === key ? "active" : ""
|
||
}">${label}</a>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
${langSwitcherHtml()}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
`;
|
||
}
|
||
|
||
function footerHtml() {
|
||
const s = Store.settings();
|
||
return `
|
||
<footer>
|
||
<div class="footer-inner">
|
||
<div>
|
||
<strong>${s.galleryName}</strong>
|
||
<div>${L(s.tagline)}</div>
|
||
<div style="margin-top:12px">${s.address}</div>
|
||
</div>
|
||
<div>
|
||
<strong>${t("footer.hours")}</strong>
|
||
<div>${L(s.hours)}</div>
|
||
</div>
|
||
<div>
|
||
<strong>${t("footer.contact")}</strong>
|
||
<a href="tel:${s.phone}">${s.phone}</a>
|
||
<a href="mailto:${s.email}">${s.email}</a>
|
||
</div>
|
||
<div>
|
||
<strong>${t("footer.follow")}</strong>
|
||
<a href="#">${t("footer.instagram")} ${s.instagram}</a>
|
||
<a href="news.html">${t("misc.newsletter")}</a>
|
||
</div>
|
||
</div>
|
||
<div class="footer-inner footer-admin">
|
||
<div>© ${new Date().getFullYear()} ${s.galleryName}. ${t("misc.rights_suffix")}</div>
|
||
<div></div>
|
||
<div></div>
|
||
<div style="text-align:right"><a href="admin/index.html">${t("misc.staff_login")}</a></div>
|
||
</div>
|
||
</footer>
|
||
`;
|
||
}
|
||
|
||
function renderChrome(active) {
|
||
mount("nav-slot", navHtml(active));
|
||
mount("footer-slot", footerHtml());
|
||
wireLangSwitcher();
|
||
// Update document title if page key maps to a known title
|
||
if (active) {
|
||
const titleKey = "title." + active;
|
||
const s = Store.settings();
|
||
document.title = `${t(titleKey)} — ${s.galleryName}`;
|
||
}
|
||
}
|