Event Detail page
This commit is contained in:
+44
-12
@@ -7,11 +7,33 @@ import Reveal from "./Reveal";
|
|||||||
import EmptyNote from "./EmptyNote";
|
import EmptyNote from "./EmptyNote";
|
||||||
import type { GalleryPhoto } from "@/lib/content";
|
import type { GalleryPhoto } from "@/lib/content";
|
||||||
|
|
||||||
const CARD =
|
const CARD_BASE = "relative [perspective:1200px] cursor-pointer text-left outline-none";
|
||||||
"relative h-52 w-72 shrink-0 sm:h-64 sm:w-[25rem] [perspective:1200px] cursor-pointer text-left outline-none";
|
/** strip = fixed size for the scrolling row; grid = fills its grid cell at a 4:3 ratio. */
|
||||||
|
const CARD_SIZE = {
|
||||||
|
strip: "h-56 w-[18rem] shrink-0 sm:h-64 sm:w-[22rem]",
|
||||||
|
grid: "w-full aspect-[4/3]",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Still row (4 or fewer photos): one row on desktop, sized so it never wraps. */
|
||||||
|
const GRID_LAYOUT: Record<number, string> = {
|
||||||
|
1: "max-w-md grid-cols-1",
|
||||||
|
2: "max-w-4xl grid-cols-1 sm:grid-cols-2",
|
||||||
|
3: "max-w-6xl grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
||||||
|
4: "max-w-7xl grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
|
||||||
|
};
|
||||||
|
|
||||||
/** One photo card: photo on the front, details on the back (flips on hover). Click opens the viewer. */
|
/** One photo card: photo on the front, details on the back (flips on hover). Click opens the viewer. */
|
||||||
function FlipCard({ photo, onOpen, hidden }: { photo: GalleryPhoto; onOpen: () => void; hidden?: boolean }) {
|
function FlipCard({
|
||||||
|
photo,
|
||||||
|
onOpen,
|
||||||
|
hidden,
|
||||||
|
size = "strip",
|
||||||
|
}: {
|
||||||
|
photo: GalleryPhoto;
|
||||||
|
onOpen: () => void;
|
||||||
|
hidden?: boolean;
|
||||||
|
size?: keyof typeof CARD_SIZE;
|
||||||
|
}) {
|
||||||
const hasDetails = Boolean(photo.alt || photo.description || photo.date);
|
const hasDetails = Boolean(photo.alt || photo.description || photo.date);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -20,7 +42,7 @@ function FlipCard({ photo, onOpen, hidden }: { photo: GalleryPhoto; onOpen: () =
|
|||||||
aria-hidden={hidden}
|
aria-hidden={hidden}
|
||||||
tabIndex={hidden ? -1 : 0}
|
tabIndex={hidden ? -1 : 0}
|
||||||
aria-label={photo.alt ? `View photo: ${photo.alt}` : "View photo"}
|
aria-label={photo.alt ? `View photo: ${photo.alt}` : "View photo"}
|
||||||
className={`group/card ${CARD}`}
|
className={`group/card ${CARD_BASE} ${CARD_SIZE[size]}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative h-full w-full rounded-2xl shadow-[0_8px_24px_-12px_rgba(11,37,69,0.35)] transition-transform duration-700 [transform-style:preserve-3d] ${
|
className={`relative h-full w-full rounded-2xl shadow-[0_8px_24px_-12px_rgba(11,37,69,0.35)] transition-transform duration-700 [transform-style:preserve-3d] ${
|
||||||
@@ -30,14 +52,24 @@ function FlipCard({ photo, onOpen, hidden }: { photo: GalleryPhoto; onOpen: () =
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* front */}
|
{/* front */}
|
||||||
<div className="absolute inset-0 overflow-hidden rounded-2xl bg-rotary-paper ring-1 ring-rotary-navy/[0.06] [backface-visibility:hidden]">
|
<div className="absolute inset-0 overflow-hidden rounded-2xl bg-rotary-navy ring-1 ring-rotary-navy/[0.06] [backface-visibility:hidden]">
|
||||||
|
{/* canvas: a blurred copy fills the frame, so portrait and landscape photos both fit without cropping */}
|
||||||
|
<Image
|
||||||
|
src={photo.src}
|
||||||
|
alt=""
|
||||||
|
aria-hidden
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 640px) 360px, 288px"
|
||||||
|
unoptimized={photo.src.startsWith("/uploads/")}
|
||||||
|
className="scale-125 object-cover opacity-70 blur-2xl"
|
||||||
|
/>
|
||||||
<Image
|
<Image
|
||||||
src={photo.src}
|
src={photo.src}
|
||||||
alt={hidden ? "" : photo.alt || "Rotary Club of Vellore photo"}
|
alt={hidden ? "" : photo.alt || "Rotary Club of Vellore photo"}
|
||||||
fill
|
fill
|
||||||
sizes="(min-width: 640px) 400px, 288px"
|
sizes="(min-width: 1024px) 360px, (min-width: 640px) 50vw, 100vw"
|
||||||
unoptimized={photo.src.startsWith("/uploads/")}
|
unoptimized={photo.src.startsWith("/uploads/")}
|
||||||
className="object-cover object-top"
|
className="object-contain drop-shadow-[0_4px_16px_rgba(0,0,0,0.25)]"
|
||||||
/>
|
/>
|
||||||
{photo.alt && (
|
{photo.alt && (
|
||||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-rotary-navy/85 to-transparent px-4 pb-3 pt-10 [@media(hover:hover)]:hidden">
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-rotary-navy/85 to-transparent px-4 pb-3 pt-10 [@media(hover:hover)]:hidden">
|
||||||
@@ -169,8 +201,8 @@ function Lightbox({
|
|||||||
export default function Gallery({ photos }: { photos: GalleryPhoto[] }) {
|
export default function Gallery({ photos }: { photos: GalleryPhoto[] }) {
|
||||||
const [open, setOpen] = useState<number | null>(null);
|
const [open, setOpen] = useState<number | null>(null);
|
||||||
|
|
||||||
// Up to 3 photos: a still, centered row. More than 3: auto-scroll.
|
// Up to 4 photos: a still row. More than 4: auto-scroll.
|
||||||
const slide = photos.length > 3;
|
const slide = photos.length > 4;
|
||||||
// Repeat short lists so the strip is always wider than the screen, then
|
// Repeat short lists so the strip is always wider than the screen, then
|
||||||
// duplicate it so the loop is seamless (animation moves -50%).
|
// duplicate it so the loop is seamless (animation moves -50%).
|
||||||
const base = slide ? Array.from({ length: Math.ceil(8 / photos.length) }, () => photos).flat() : photos;
|
const base = slide ? Array.from({ length: Math.ceil(8 / photos.length) }, () => photos).flat() : photos;
|
||||||
@@ -187,9 +219,9 @@ export default function Gallery({ photos }: { photos: GalleryPhoto[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{photos.length > 0 && !slide && (
|
{photos.length > 0 && !slide && (
|
||||||
<div className="mx-auto mt-8 flex max-w-7xl flex-wrap justify-center gap-6 px-4 sm:px-6 lg:px-8">
|
<div className={`mx-auto mt-8 grid gap-5 px-4 sm:px-6 lg:px-8 ${GRID_LAYOUT[photos.length]}`}>
|
||||||
{photos.map((p, i) => (
|
{photos.map((p, i) => (
|
||||||
<FlipCard key={p.id} photo={p} onOpen={() => setOpen(i)} />
|
<FlipCard key={p.id} size="grid" photo={p} onOpen={() => setOpen(i)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +233,7 @@ export default function Gallery({ photos }: { photos: GalleryPhoto[] }) {
|
|||||||
style={{ animationDuration: `${base.length * 5}s` }}
|
style={{ animationDuration: `${base.length * 5}s` }}
|
||||||
>
|
>
|
||||||
{loop.map((p, i) => (
|
{loop.map((p, i) => (
|
||||||
<div key={`${p.id}-${i}`} className="mr-6">
|
<div key={`${p.id}-${i}`} className="mr-5">
|
||||||
<FlipCard photo={p} hidden={i >= photos.length} onOpen={() => setOpen(i % photos.length)} />
|
<FlipCard photo={p} hidden={i >= photos.length} onOpen={() => setOpen(i % photos.length)} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user