feat: memberships page

This commit is contained in:
2026-06-04 17:19:46 +07:00
parent 0e1bf168ed
commit 1e043d53a3
222 changed files with 582 additions and 87 deletions
+31 -31
View File
@@ -18,7 +18,7 @@ type EventItem = {
export default function GalleryPageClient() {
const { gallery, lang } = useTranslation();
// Active Event Index (0: latest event, 7: oldest event)
const [activeEventIndex, setActiveEventIndex] = useState(0);
@@ -128,21 +128,21 @@ export default function GalleryPageClient() {
{/* Gallery Section */}
<section className="py-12 lg:py-20 px-[var(--spacing-gutter)] max-w-hd mx-auto">
{/* Step Timeline Navigation */}
<div className="mb-10 max-w-4xl mx-auto px-4">
<div className="relative flex justify-between items-center">
{/* Timeline track behind steps */}
<div className="absolute left-0 right-0 h-[2px] bg-white/10 -z-10" />
<div
<div
className="absolute left-0 h-[2px] bg-primary transition-all duration-500 -z-10"
style={{ width: `${(activeEventIndex / (gallery.events.length - 1)) * 100}%` }}
/>
{gallery.events.map((event: EventItem, idx: number) => {
const isActive = idx === activeEventIndex;
const isPassed = idx < activeEventIndex;
return (
<button
key={event.id}
@@ -152,13 +152,13 @@ export default function GalleryPageClient() {
title={event.title}
>
{/* Step circle */}
<div
<div
className={[
"w-8 h-8 rounded-full flex items-center justify-center font-[var(--font-label-caps)] text-xs font-bold transition-all duration-300 border-2 z-10",
isActive
? "bg-background border-primary text-primary status-led shadow-[0_0_12px_rgba(197,160,89,0.6)] scale-110"
: isPassed
? "bg-primary border-primary text-on-primary"
isActive
? "bg-background border-primary text-primary status-led shadow-[0_0_12px_rgba(197,160,89,0.6)] scale-110"
: isPassed
? "bg-primary border-primary text-on-primary"
: "bg-surface-container-high border-white/10 text-on-surface-variant/40 hover:border-white/30 hover:text-on-surface"
].join(" ")}
>
@@ -173,7 +173,7 @@ export default function GalleryPageClient() {
{/* Active Event Grid Container */}
{activeEvent && (
<div className="glass-panel p-6 sm:p-10 lg:p-16 rounded-[2rem] lg:rounded-[3rem] border border-primary/20 hover:border-primary/30 shadow-2xl transition-all duration-300">
{/* Top Navigation Control Header */}
<div className="flex justify-between items-center mb-10 border-b border-white/5 pb-6">
<button
@@ -187,8 +187,8 @@ export default function GalleryPageClient() {
</button>
<span className="font-[var(--font-label-caps)] text-xs sm:text-sm text-primary tracking-widest bg-primary/5 border border-primary/20 px-4 py-2 rounded-xl">
{lang === "vi"
? `SỰ KIỆN ${activeEventIndex + 1} / ${gallery.events.length}`
{lang === "vi"
? `SỰ KIỆN ${activeEventIndex + 1} / ${gallery.events.length}`
: `EVENT ${activeEventIndex + 1} OF ${gallery.events.length}`
}
</span>
@@ -209,13 +209,13 @@ export default function GalleryPageClient() {
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-[var(--font-display-lg)] text-primary mb-4 leading-tight">
{activeEvent.title}
</h2>
<div className="flex flex-wrap items-center gap-4 text-xs uppercase tracking-wider text-on-surface-variant/50 font-[var(--font-label-caps)] mb-6">
<div className="flex items-center gap-1.5 bg-surface-container-high px-3.5 py-2 rounded-xl border border-white/5">
<span className="material-symbols-outlined text-base text-primary">calendar_today</span>
<span>{activeEvent.date}</span>
</div>
{activeEvent.googleDriveUrl && activeEvent.googleDriveUrl.trim() !== "" && (
<a
href={activeEvent.googleDriveUrl}
@@ -235,8 +235,8 @@ export default function GalleryPageClient() {
<div className="flex flex-col items-center justify-center py-20 px-6 border border-white/5 bg-white/[0.01] rounded-2xl">
<span className="material-symbols-outlined text-5xl text-primary/45 mb-4">image</span>
<p className="text-on-surface-variant/60 font-sans text-sm sm:text-base text-center max-w-md">
{lang === "vi"
? "Hình ảnh nổi bật cho sự kiện này hiện đang được cập nhật."
{lang === "vi"
? "Hình ảnh nổi bật cho sự kiện này hiện đang được cập nhật."
: "Highlight images for this event are currently being updated."
}
</p>
@@ -244,7 +244,7 @@ export default function GalleryPageClient() {
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{activeEvent.images.map((imgUrl, i) => (
<div
<div
key={i}
className={[
getMosaicClasses(i, activeEvent.images.length),
@@ -253,9 +253,9 @@ export default function GalleryPageClient() {
onClick={() => openLightbox(i)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getImgSrc(imgUrl)}
alt={`${activeEvent.title} Highlight ${i + 1}`}
<img
src={getImgSrc(imgUrl)}
alt={`${activeEvent.title} Highlight ${i + 1}`}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
loading="lazy"
/>
@@ -278,13 +278,13 @@ export default function GalleryPageClient() {
{/* Lightbox Overlay */}
{lightboxImageIndex !== null && activeEvent && activeImage && (
<div
<div
className="fixed inset-0 z-[100] flex flex-col justify-center items-center bg-black/95 backdrop-blur-md transition-opacity duration-300"
onClick={closeLightbox}
>
{/* Close button */}
<button
type="button"
<button
type="button"
className="absolute top-6 right-6 text-white/70 hover:text-white transition-colors p-2 z-[110] focus:outline-none"
onClick={closeLightbox}
aria-label="Close Lightbox"
@@ -293,12 +293,12 @@ export default function GalleryPageClient() {
</button>
{/* Lightbox content container */}
<div
<div
className="relative flex items-center justify-between w-full max-w-6xl px-4 md:px-12 h-[60vh] sm:h-[70vh] md:h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Prev arrow */}
<button
<button
type="button"
className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/5 hover:bg-white/10 border border-white/10 text-white flex items-center justify-center transition-colors mr-2 active:scale-95 focus:outline-none"
onClick={() => setLightboxImageIndex((prev) => (prev! - 1 + activeEvent.images.length) % activeEvent.images.length)}
@@ -310,15 +310,15 @@ export default function GalleryPageClient() {
{/* Active Image */}
<div className="flex-1 flex items-center justify-center h-full relative overflow-hidden select-none px-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getImgSrc(activeImage)}
<img
src={getImgSrc(activeImage)}
alt={`${activeEvent.title} Full Highlight`}
className="max-w-full max-h-full object-contain rounded-lg border border-white/5 shadow-2xl transition-all duration-300 animate-in fade-in zoom-in-95 duration-200"
/>
</div>
{/* Next arrow */}
<button
<button
type="button"
className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/5 hover:bg-white/10 border border-white/10 text-white flex items-center justify-center transition-colors ml-2 active:scale-95 focus:outline-none"
onClick={() => setLightboxImageIndex((prev) => (prev! + 1) % activeEvent.images.length)}
@@ -329,7 +329,7 @@ export default function GalleryPageClient() {
</div>
{/* Info bar below image */}
<div
<div
className="text-center mt-6 px-6 max-w-2xl select-none"
onClick={(e) => e.stopPropagation()}
>
@@ -337,7 +337,7 @@ export default function GalleryPageClient() {
{activeEvent.title}
</h3>
<p className="text-on-surface-variant/60 text-xs sm:text-sm uppercase tracking-wider font-[var(--font-label-caps)]">
{lang === "vi"
{lang === "vi"
? `Hình ảnh ${lightboxImageIndex + 1} trên ${activeEvent.images.length}`
: `Image ${lightboxImageIndex + 1} of ${activeEvent.images.length}`
}