feat: initial Jimi Gallery prototype
- 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
This commit is contained in:
129
assets/app.js
Normal file
129
assets/app.js
Normal file
@ -0,0 +1,129 @@
|
||||
// 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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user