image update

This commit is contained in:
2026-09-25 14:41:56 +05:30
parent 3773b3c1ed
commit 558bd0e629
7 changed files with 156 additions and 46 deletions
+11 -1
View File
@@ -339,7 +339,17 @@ function EventCard({
</label>
<label className="block text-xs font-medium text-rotary-navy">
Date
<input value={draft.date} placeholder="e.g. 12 Aug 2026" onChange={(e) => setDraft({ date: e.target.value })} className={`${input} mt-1`} />
<input
type="date"
value={/^\d{4}-\d{2}-\d{2}$/.test(draft.date) ? draft.date : ""}
onChange={(e) => setDraft({ date: e.target.value })}
className={`${input} mt-1`}
/>
{draft.date && !/^\d{4}-\d{2}-\d{2}$/.test(draft.date) && (
<span className="mt-1 block text-[11px] font-normal text-rotary-amber">
Currently “{draft.date}” — pick a date to replace it
</span>
)}
</label>
</div>
<label className="block text-xs font-medium text-rotary-navy">
+12 -13
View File
@@ -1,10 +1,12 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import ViewableImage from "@/components/ViewableImage";
import { notFound } from "next/navigation";
import DetailShell, { Chip } from "@/components/DetailShell";
import RichText from "@/components/RichText";
import { getContent } from "@/lib/content";
import { formatEventDate } from "@/lib/meetings";
export const dynamic = "force-dynamic";
@@ -36,21 +38,18 @@ export default async function EventPage({ params }: Props) {
title={event.title}
backHref="/#events"
backLabel="All events"
badge={event.date ? <Chip tone="gold">{event.date}</Chip> : undefined}
badge={event.date ? <Chip tone="gold">{formatEventDate(event.date)}</Chip> : undefined}
>
<article className="overflow-hidden rounded-3xl bg-white shadow-[0_20px_50px_-24px_rgba(3,20,58,0.35)] ring-1 ring-rotary-navy/[0.06]">
{event.image && (
<div className="relative aspect-[16/9] w-full bg-rotary-navy/[0.04]">
<Image
src={event.image}
alt={event.title}
fill
priority
sizes="(min-width: 896px) 896px, 100vw"
unoptimized={event.image.startsWith("/uploads/")}
className="object-contain"
/>
</div>
<ViewableImage
src={event.image}
alt={event.title}
caption={event.title}
priority
sizes="(min-width: 1024px) 1024px, 100vw"
frameClassName="aspect-[16/9] w-full bg-rotary-navy/[0.04]"
/>
)}
<div className="p-6 sm:p-10">
{event.text && (
@@ -87,7 +86,7 @@ export default async function EventPage({ params }: Props) {
)}
</div>
<div className="p-4">
{e.date && <p className="text-[11px] font-semibold uppercase tracking-wider text-rotary-azure">{e.date}</p>}
{e.date && <p className="text-[11px] font-semibold uppercase tracking-wider text-rotary-azure">{formatEventDate(e.date)}</p>}
<p className="mt-0.5 font-semibold text-rotary-navy group-hover:text-rotary-blue">{e.title}</p>
</div>
</Link>
+9 -18
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import Image from "next/image";
import ViewableImage from "@/components/ViewableImage";
import { notFound } from "next/navigation";
import DetailShell, { Chip } from "@/components/DetailShell";
import RichText from "@/components/RichText";
@@ -101,23 +101,14 @@ export default async function MeetingPage({ params }: Props) {
</article>
{meeting.poster && (
<a
href={meeting.poster}
target="_blank"
rel="noopener noreferrer"
className="relative block aspect-[4/5] overflow-hidden rounded-3xl bg-white shadow-[0_20px_50px_-24px_rgba(3,20,58,0.35)] ring-4 ring-white"
title="Open poster"
>
<Image
src={meeting.poster}
alt={meeting.topic || "Meeting poster"}
fill
priority
sizes="(min-width: 1024px) 320px, 100vw"
unoptimized={meeting.poster.startsWith("/uploads/")}
className="object-contain"
/>
</a>
<ViewableImage
src={meeting.poster}
alt={meeting.topic || "Meeting poster"}
caption={meeting.topic}
priority
sizes="(min-width: 1024px) 320px, 100vw"
frameClassName="aspect-[4/5] w-full rounded-3xl bg-white shadow-[0_20px_50px_-24px_rgba(3,20,58,0.35)] ring-4 ring-white"
/>
)}
</div>
</DetailShell>
+8 -10
View File
@@ -1,5 +1,6 @@
import Image from "next/image";
import Link from "next/link";
import ViewableImage from "./ViewableImage";
import Reveal from "./Reveal";
import type { MeetingItem } from "@/lib/content";
import { formatMeetingDate } from "@/lib/meetings";
@@ -114,16 +115,13 @@ export default function EventDetails({ next }: { next?: MeetingItem }) {
{/* Right: next meeting poster, next meeting card, or "no upcoming events" */}
<Reveal delay={120}>
{poster ? (
<div className="relative h-80 w-full overflow-hidden rounded-[1.5rem] bg-white shadow-[0_20px_50px_-20px_rgba(3,20,58,0.35)] ring-4 ring-white sm:h-full sm:min-h-[22rem]">
<Image
src={poster}
unoptimized={poster.startsWith("/uploads/")}
alt={next?.topic || "Upcoming meeting poster"}
fill
sizes="(min-width: 1024px) 560px, 100vw"
className="object-contain"
/>
</div>
<ViewableImage
src={poster}
alt={next?.topic || "Upcoming meeting poster"}
caption={next?.topic}
sizes="(min-width: 1024px) 560px, 100vw"
frameClassName="h-80 w-full rounded-[1.5rem] bg-white shadow-[0_20px_50px_-20px_rgba(3,20,58,0.35)] ring-4 ring-white sm:h-full sm:min-h-[22rem]"
/>
) : next ? (
<div className="flex h-80 w-full flex-col items-center justify-center rounded-[1.5rem] bg-gradient-to-br from-rotary-navy to-rotary-blue px-6 text-center text-white shadow-[0_20px_50px_-20px_rgba(3,20,58,0.35)] sm:h-full sm:min-h-[22rem]">
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-rotary-goldlight">
+7 -3
View File
@@ -3,6 +3,7 @@
/* eslint-disable @next/next/no-img-element */
import Image from "next/image";
import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import Reveal from "./Reveal";
import EmptyNote from "./EmptyNote";
import type { GalleryPhoto } from "@/lib/content";
@@ -241,9 +242,12 @@ export default function Gallery({ photos }: { photos: GalleryPhoto[] }) {
</div>
)}
{open !== null && photos[open] && (
<Lightbox photos={photos} index={open} onClose={() => setOpen(null)} onGo={setOpen} />
)}
{open !== null &&
photos[open] &&
createPortal(
<Lightbox photos={photos} index={open} onClose={() => setOpen(null)} onGo={setOpen} />,
document.body,
)}
</section>
);
}
+103
View File
@@ -0,0 +1,103 @@
"use client";
/* eslint-disable @next/next/no-img-element */
import Image from "next/image";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
/**
* An image inside a frame; clicking it opens a full-screen viewer.
* Used for meeting posters and event photos.
*/
export default function ViewableImage({
src,
alt,
frameClassName,
sizes,
priority,
caption,
}: {
src: string;
alt: string;
frameClassName: string;
sizes: string;
priority?: boolean;
caption?: string;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
window.addEventListener("keydown", onKey);
const overflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
window.removeEventListener("keydown", onKey);
document.body.style.overflow = overflow;
};
}, [open]);
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
aria-label={`View full size: ${alt}`}
className={`group relative block cursor-zoom-in overflow-hidden text-left ${frameClassName}`}
>
<Image
src={src}
alt={alt}
fill
priority={priority}
sizes={sizes}
unoptimized={src.startsWith("/uploads/")}
className="object-contain transition-transform duration-500 group-hover:scale-[1.02]"
/>
<span className="pointer-events-none absolute bottom-3 right-3 flex items-center gap-1.5 rounded-full bg-rotary-navy/80 px-3 py-1.5 text-xs font-medium text-white opacity-90 backdrop-blur transition-opacity group-hover:opacity-100">
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="7" />
<path d="M21 21l-4.3-4.3M11 8v6M8 11h6" strokeLinecap="round" />
</svg>
View
</span>
</button>
{open &&
createPortal(
<div
role="dialog"
aria-modal="true"
aria-label={alt}
onClick={() => setOpen(false)}
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-rotary-navy/95 p-4 backdrop-blur-sm animate-fade-up"
>
<button
type="button"
aria-label="Close"
onClick={() => setOpen(false)}
className="absolute right-4 top-4 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
</svg>
</button>
<img
src={src}
alt={alt}
onClick={(e) => e.stopPropagation()}
className="max-h-[85vh] w-auto max-w-full rounded-xl object-contain shadow-2xl"
/>
<div className="mt-4 flex items-center gap-4 text-sm text-white/70" onClick={(e) => e.stopPropagation()}>
{caption && <span className="font-medium text-white">{caption}</span>}
<a href={src} target="_blank" rel="noopener noreferrer" className="underline-offset-4 hover:text-white hover:underline">
Open original ↗
</a>
</div>
</div>,
document.body,
)}
</>
);
}
+6 -1
View File
@@ -23,6 +23,11 @@ export function formatMeetingDate(date: string) {
});
}
/** Event dates are saved as YYYY-MM-DD (date picker); older free-text dates are shown as typed. */
export function formatEventDate(date: string) {
return /^\d{4}-\d{2}-\d{2}$/.test(date) ? formatMeetingDate(date) : date;
}
/** A card in the website's "Past Events" list: a manual event or a completed meeting. */
export type PastEventCard = {
id: string;
@@ -55,7 +60,7 @@ export function buildPastEvents(
id: e.id,
href: `/events/${e.id}`,
image: e.image,
date: e.date,
date: formatEventDate(e.date),
title: e.title,
text: e.text,
kind: "event" as const,