Files
jimi-gallery/exhibition.html
yakenator 098b55e3b0 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
2026-04-25 12:47:36 +09:00

213 lines
8.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Exhibition — Jimi Gallery</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600&family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="assets/styles.css" />
</head>
<body>
<div id="nav-slot"></div>
<main id="content"></main>
<div id="footer-slot"></div>
<div id="slider" class="slider" role="dialog" aria-modal="true" aria-hidden="true"></div>
<script src="assets/i18n.js"></script>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
(async () => {
try { await Store.load(); } catch (e) { return showBootError(e); }
renderChrome("exhibition");
const id = qs("id");
const e = Store.exhibition(id);
if (!e) {
mount(
"content",
`<section class="section"><h1>${t("misc.not_found")}</h1><p><a href="exhibitions.html">${t("cta.all_exhibitions")}</a></p></section>`
);
} else {
const artists = e.artistIds.map((id) => Store.artist(id)).filter(Boolean);
const artistNames = artists.map((a) => a.name).join(", ");
const works = artists.flatMap((a) =>
(a.works || []).map((w) => ({ ...w, artist: a.name }))
);
mount(
"content",
`
<section class="hero">
<img class="hero-image slider-trigger" data-slider-index="hero" src="${e.hero}" alt="${L(e.title)}" />
<div class="hero-meta">
<div>
<div class="eyebrow">${t("status." + e.status)}</div>
<h1>${artistNames}<br><em style="font-style:italic">${L(e.title)}</em></h1>
</div>
<div class="hero-sub">
<strong>${fmtRange(e.startDate, e.endDate)}</strong>
${e.venue}
</div>
</div>
</section>
<hr class="divider" />
<section class="section" style="display:grid;grid-template-columns:1fr 1fr;gap:80px">
<div>
<div class="eyebrow">${t("eyebrow.about_exhibition")}</div>
</div>
<div>
<p style="font-size:18px;line-height:1.6">${L(e.description)}</p>
${e.press ? `<p class="muted">${L(e.press)}</p>` : ""}
<p style="margin-top:32px">
${artists
.map(
(a) => `<a href="artist.html?id=${a.id}" style="text-decoration:underline">${t("cta.about_prefix")} ${a.name}</a>`
)
.join("<br>")}
</p>
</div>
</section>
${
works.length
? `<hr class="divider"/>
<section class="section">
<div class="eyebrow">${t("eyebrow.works_in_exhibition")}</div>
<div class="works-grid" style="margin-top:32px">
${works
.map(
(w, i) => `
<figure class="work">
<img class="slider-trigger" data-slider-index="${i}" src="${w.image}" alt="${L(w.title)}"/>
<figcaption>
<strong>${w.artist}</strong>
<em style="font-style:italic">${L(w.title)}</em>, ${w.year}<br>
${L(w.medium)}<br>${w.dimensions}
</figcaption>
</figure>
`
)
.join("")}
</div>
</section>`
: ""
}
`
);
// --- Slider / lightbox ---
const slides = works.map((w) => ({
image: w.image,
title: L(w.title),
artist: w.artist,
year: w.year,
medium: L(w.medium),
dimensions: w.dimensions
}));
const sliderEl = document.getElementById("slider");
let sliderIdx = 0;
function renderSlider() {
if (!slides.length) return;
const s = slides[sliderIdx];
sliderEl.innerHTML = `
<div class="slider-header">
<span class="slider-counter">${sliderIdx + 1} / ${slides.length}</span>
<button class="slider-close" aria-label="Close">×</button>
</div>
<div class="slider-stage">
<button class="slider-nav prev" aria-label="Previous" ${sliderIdx === 0 ? "disabled" : ""}></button>
<img src="${s.image}" alt="${s.title}" />
<button class="slider-nav next" aria-label="Next" ${sliderIdx === slides.length - 1 ? "disabled" : ""}></button>
</div>
<div class="slider-caption">
<strong>${s.title}</strong>
${s.artist} · ${s.year} · ${s.medium} · ${s.dimensions}
</div>
<div class="slider-strip">
${slides
.map(
(x, i) =>
`<button data-idx="${i}" class="${i === sliderIdx ? "active" : ""}"><img src="${x.image}" alt=""></button>`
)
.join("")}
</div>
`;
sliderEl.querySelector(".slider-close").addEventListener("click", closeSlider);
sliderEl.querySelector(".slider-nav.prev").addEventListener("click", () => goSlider(-1));
sliderEl.querySelector(".slider-nav.next").addEventListener("click", () => goSlider(1));
sliderEl.querySelectorAll(".slider-strip button").forEach((b) => {
b.addEventListener("click", () => jumpSlider(parseInt(b.dataset.idx, 10)));
});
// scroll active thumb into view
const activeThumb = sliderEl.querySelector(".slider-strip button.active");
if (activeThumb) activeThumb.scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
}
function openSlider(idx) {
if (!slides.length) return;
sliderIdx = Math.max(0, Math.min(slides.length - 1, idx || 0));
sliderEl.classList.add("open");
sliderEl.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
const p = new URLSearchParams(location.search);
p.set("view", "slider");
history.replaceState(null, "", location.pathname + "?" + p.toString() + "#" + (sliderIdx + 1));
renderSlider();
}
function closeSlider() {
sliderEl.classList.remove("open");
sliderEl.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
const p = new URLSearchParams(location.search);
p.delete("view");
const qStr = p.toString();
history.replaceState(null, "", location.pathname + (qStr ? "?" + qStr : ""));
}
function goSlider(delta) {
const next = sliderIdx + delta;
if (next < 0 || next >= slides.length) return;
sliderIdx = next;
history.replaceState(null, "", location.pathname + location.search + "#" + (sliderIdx + 1));
renderSlider();
}
function jumpSlider(idx) {
if (idx < 0 || idx >= slides.length) return;
sliderIdx = idx;
history.replaceState(null, "", location.pathname + location.search + "#" + (sliderIdx + 1));
renderSlider();
}
document.querySelectorAll(".slider-trigger").forEach((el) => {
el.addEventListener("click", () => {
const raw = el.dataset.sliderIndex;
if (raw === "hero") openSlider(0);
else openSlider(parseInt(raw, 10) || 0);
});
});
document.addEventListener("keydown", (ev) => {
if (!sliderEl.classList.contains("open")) return;
if (ev.key === "Escape") closeSlider();
else if (ev.key === "ArrowLeft") goSlider(-1);
else if (ev.key === "ArrowRight") goSlider(1);
});
// Deep-link: ?view=slider#N
if (qs("view") === "slider" && slides.length) {
const hashIdx = parseInt(location.hash.replace("#", ""), 10);
openSlider(isFinite(hashIdx) && hashIdx > 0 ? hashIdx - 1 : 0);
}
}
})();
</script>
</body>
</html>