import { useState } from "react"; import PageShell from "../components/PageShell.tsx" import eventsData from "../events.js"; const LOGO_FILES = import.meta.glob("../assets/event-logos/*.svg", { eager: true, import: "default", }); const LOGOS = Object.fromEntries( Object.entries(LOGO_FILES).map(([path, src]) => [path.split("/").pop(), src]) ); const logoSrc = file => (file && LOGOS[file]) || null; // Used when an event doesn't name its own org_logo. const DEFAULT_ORG_LOGO = null; /* An that removes itself if the file 404s. */ function Logo({ file, alt = "", className }) { const [failed, setFailed] = useState(false); const src = logoSrc(file); if (!src || failed) return null; return ( {alt} setFailed(true)} /> ); } const TEAL = "#138ba0"; // page heading + last-resort card color /* ── Grid view ──────────────────────────────────────────────── The grid fits as many columns as the screen allows, with no breakpoints: each card is at least CARD_MIN wide, and the columns share whatever space is left over. CARD_MIN narrowest a card may get before dropping a column MAX_COLS ceiling on columns, so cards don't get absurd on very wide monitors. GRID_MAX is derived from it. ───────────────────────────────────────────────────────────── */ const CARD_MIN = "32rem"; const MAX_COLS = 4; const GRID_MAX = `calc(${MAX_COLS} * 38rem)`; const GRID_TEMPLATE = `repeat(auto-fill, minmax(min(${CARD_MIN}, 100%), 1fr))`; /* Collapsed past-events strip: how much shows, and the downward fade applied while it's collapsed. */ const PAST_PEEK = "10rem"; const PAST_FADE = "linear-gradient(to bottom, black 0%, black 45%, transparent 100%)"; /* ── Carousel sizing ────────────────────────────────────────── CARD the card itself (your original max-w-3xl) GAP space between cards — raise to push neighbors out FADE_DIST how far past the card's edge neighbors fade to nothing ───────────────────────────────────────────────────────────── */ const CARD = "min(48rem, 90vw)"; const GAP = "5rem"; const SLIDE = `calc(${CARD} + ${GAP})`; const HALF_SLIDE = `calc(${CARD} / 2)`; const FADE_DIST = "18rem"; const EDGE_FADE = `linear-gradient(to right, transparent calc(50% - (${HALF_SLIDE} + ${FADE_DIST})), black calc(50% - ${HALF_SLIDE}), black calc(50% + ${HALF_SLIDE}), transparent calc(50% + (${HALF_SLIDE} + ${FADE_DIST})))`; /* ── Grid-card layout ───────────────────────────────────────── Container queries, not media queries: each card reacts to its OWN width, which is what we need now that the column count is dynamic and the viewport says nothing about how wide a card is. Wide card NGU logo + details on the left, event logo on the right, descriptions full-width underneath. Narrow card logos on their own row, then details, then descriptions — three stacked rows. ───────────────────────────────────────────────────────────── */ const CARD_STYLES = ` .ev-card { container-type: inline-size; } .ev-grid { display: grid; gap: 0.25rem 1.5rem; grid-template-columns: 2fr 1fr; grid-template-areas: "ngu image" "info image" "desc desc"; } @container (max-width: 30rem) { .ev-grid { grid-template-columns: 1fr; grid-template-areas: "ngu" "info" "desc"; } .ev-grid .ev-image { justify-self: start; width: auto; max-height: 7rem; margin-bottom: 0.5rem; } } .ev-ngu { grid-area: ngu; } .ev-info { grid-area: info; } .ev-image { grid-area: image; justify-self: stretch; align-self: center; max-height: 11rem; } .ev-desc { grid-area: desc; margin-top: 1rem; } `; const InstagramIcon = ({ id = "ig-gradient" }) => ( ); /* ═══════════════════════════════════════════════════════════════ EVENT CARD — one component, two sizes. compact=false → the full card used in the carousel compact=true → the grid card: details left, text right, and a footer pinned to the bottom so every card in a row is the same height with aligned buttons. ═══════════════════════════════════════════════════════════════ */ export function Card({ ev, defaultColor = TEAL, accent = TEAL, compact = false, interactive = true, }) { const past = ev.status === "past"; const color = ev.color || defaultColor; const orgLogo = ev.org_logo || DEFAULT_ORG_LOGO; const eventLogo = ev.image; const igHandle = ev.instagram || null; const igUrl = igHandle ? `https://instagram.com/${igHandle.replace(/^@/, "")}` : null; const descriptions = ( <> {ev.desc_a && (

{ev.desc_a}

)} {ev.desc_b && (

{ev.desc_b}

)} ); return (
{compact ? ( /* ── GRID CARD: layout driven by .ev-grid container queries ── */

{ev.title}

{ev.theme && (

"{ev.theme}"

)} {ev.date &&

{ev.date}

} {ev.location &&

{ev.location}

}
{descriptions}
) : ( /* ── FULL: details left, large event logo right ── */ <>

{ev.title}

{ev.theme && (

"{ev.theme}"

)} {ev.date &&

{ev.date}

} {ev.location &&

{ev.location}

}
{descriptions} )} {/* Footer — mt-auto pins it to the bottom so buttons line up across every card in a grid row. */} {ev.links.length > 0 ? (
{ev.links.map(item => ( {item.label} ))}
) : past ? (

This event has concluded — thank you to everyone who joined us!

) : (

{igHandle ? "Registration has not opened yet, follow our instagram for more details." : "Registration has not opened yet — check back soon for more details."}

{igHandle && ( {igHandle} )}
)}
); } /* ═══════════════════════════════════════════════════════════════ VIEW TOGGLE — carousel / grid segmented control ═══════════════════════════════════════════════════════════════ */ function ViewToggle({ view, setView, accent }) { const btn = active => ({ background: active ? accent : "transparent", color: active ? "#ffffff" : accent, }); return (
); } /* ═══════════════════════════════════════════════════════════════ SECTION — heading + toggle, then carousel or grid ═══════════════════════════════════════════════════════════════ */ export function CardSection({ section, view }) { const { events, accent, defaultColor, } = section; const startIndex = (() => { const i = events.findIndex(e => e.status === "upcoming"); return i === -1 ? Math.max(0, events.length - 1) : i; })(); const [index, setIndex] = useState(startIndex); const [showPast, setShowPast] = useState(false); // Grid view splits the list; the carousel still shows everything. const upcoming = events.filter(e => e.status !== "past"); const pastEvents = events.filter(e => e.status === "past"); const prev = () => setIndex(i => Math.max(0, i - 1)); const next = () => setIndex(i => Math.min(events.length - 1, i + 1)); // Arrows and dots follow the section accent, not the active card. const arrowStyle = enabled => ({ border: `1px solid ${accent}`, background: "rgba(255,255,255,0.85)", color: enabled ? accent : "#b8c6c9", cursor: enabled ? "pointer" : "default", opacity: enabled ? 1 : 0.4, }); return (
{events.length === 0 ? (

· Events coming soon, stay connected for announcements ·

) : view === "grid" ? ( /* ── GRID VIEW — upcoming first, past events collapsed below ── */
{upcoming.length > 0 && (
{upcoming.map(ev => ( ))}
)} {pastEvents.length > 0 && (

Past Events

{/* Collapsed: clipped to PAST_PEEK and faded out at the bottom. Expanded: full height, no mask. */}
{pastEvents.map(ev => ( ))}
)}
) : ( /* ── CAROUSEL VIEW ── */ <>
{events.map((ev, i) => { const active = i === index; return (
!active && setIndex(i)} aria-hidden={!active} >
); })}
{events.length > 1 && ( <> )}
{events.length > 1 && (
{events.map((e, i) => (
)} )}
); } /* ═══════════════════════════════════════════════════════════════ RETREATS PAGE — hands the card sections to the shared PageShell so this page matches Community, Leadership and Resources. View state lives here, one entry per section, so the toggle in a section's action bar and the cards below it stay in sync. ═══════════════════════════════════════════════════════════════ */ export default function RetreatsPage() { const [views, setViews] = useState(() => Object.fromEntries( eventsData.sections.map(s => [s.id, s.defaultView || "carousel"]) ) ); const setView = (id, value) => setViews(prev => ({ ...prev, [id]: value })); const shellSections = eventsData.sections.map(section => { const view = views[section.id]; return { id: section.id, title: section.title, blurb: section.blurb, accent: section.accent, background: section.background, actions: section.events.length > 0 && ( setView(section.id, value)} accent={section.accent} /> ), content: , }; }); return ( <> ); }