Initial events Astro site

This commit is contained in:
Neeldhara Misra 2026-07-05 23:36:04 +05:30
commit 69a5937ead
41 changed files with 12166 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
.astro/
.DS_Store
npm-debug.log*

18
README.md Normal file
View file

@ -0,0 +1,18 @@
# Events
Astro rebuild of the events site at `events.neeldhara.com`.
## Development
```sh
npm install
npm run dev
```
## Build
```sh
npm run build
```
The site is static and builds to `dist/`.

10
astro.config.mjs Normal file
View file

@ -0,0 +1,10 @@
// @ts-check
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import { defineConfig } from "astro/config";
export default defineConfig({
site: "https://events.neeldhara.com",
integrations: [mdx(), sitemap()],
output: "static",
});

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

7175
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

19
package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "events",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "astro check"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/mdx": "^7.0.2",
"@astrojs/sitemap": "^3.7.3",
"astro": "^7.0.6",
"typescript": "^5.9.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

5
public/favicon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Events">
<rect width="64" height="64" rx="14" fill="#f7efe5"/>
<path d="M15 22.5h34M15 32h26M15 41.5h30" fill="none" stroke="#2f3f38" stroke-width="4" stroke-linecap="round"/>
<circle cx="48" cy="20" r="6" fill="#d97757"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,358 @@
---
import type { Event, EventPageEntry } from "../data/events";
import {
barcampParticipants,
barcampProposalDocUrl,
barcampRegistrationUrl,
barcampTracks,
getBarcampTrackScheduleGroups,
} from "../data/barcamp";
import { getEventBasePath } from "../data/events";
import { getEventNavItems } from "../data/eventNav";
import SiteLayout from "../layouts/SiteLayout.astro";
type Props = {
event: Event;
pageEntry?: EventPageEntry;
};
const { event } = Astro.props;
const basePath = getEventBasePath(event);
const pageEntry = Astro.props.pageEntry ?? {
id: "overview",
label: "Overview",
href: basePath,
type: "overview",
};
const eventTitle = event.shortTitle ?? event.title;
const isOverview = pageEntry.type === "overview";
const pageTitle = isOverview ? eventTitle : `${pageEntry.label} - ${eventTitle}`;
const navItems = getEventNavItems(event);
const trackScheduleGroups = {
"track-a": getBarcampTrackScheduleGroups("track-a"),
"track-b": getBarcampTrackScheduleGroups("track-b"),
};
---
<SiteLayout
title={pageTitle}
description={event.summary}
theme="barcamp"
brandHref={basePath}
brandTitle={eventTitle}
brandSubtitle="Event home"
navItems={navItems}
>
{isOverview ? (
<section class="barcamp-hero">
<div class="barcamp-hero__copy">
<p class="eyebrow">Unconference / July 18, 2026</p>
<h1>Barcamp 2026</h1>
<p class="lede">
A participant-led day at IIT Gandhinagar for short talks, demos, open discussions,
workshops, questions, experiments, and useful hallway conversations.
</p>
<div class="hero-actions">
<a class="button button--barcamp" href={barcampRegistrationUrl}>Register for Barcamp 2026</a>
<a class="button button--secondary" href={`${basePath}schedule/`}>View schedule</a>
</div>
</div>
<div
class="barcamp-board"
aria-hidden="true"
>
<span class="barcamp-board__rail"></span>
<span class="barcamp-board__card barcamp-board__card--one">Track A</span>
<span class="barcamp-board__card barcamp-board__card--two">Track B</span>
<span class="barcamp-board__card barcamp-board__card--three">Demos</span>
<span class="barcamp-board__card barcamp-board__card--four">BoF</span>
<span class="barcamp-board__pin barcamp-board__pin--one"></span>
<span class="barcamp-board__pin barcamp-board__pin--two"></span>
</div>
</section>
) : (
<section class="event-page-heading event-page-heading--barcamp">
<nav class="event-breadcrumb" aria-label="Event breadcrumb">
<a class="event-breadcrumb__event" href={basePath}>{eventTitle}</a>
<span aria-hidden="true">/</span>
<span>{pageEntry.label}</span>
</nav>
<h1>{pageEntry.label}</h1>
</section>
)}
{isOverview && (
<div class="barcamp-page">
<section class="barcamp-facts" aria-label="Event facts">
<dl>
<div>
<dt>Date</dt>
<dd>July 18, 2026</dd>
</div>
<div>
<dt>Time</dt>
<dd>9:30 AM - 6:00 PM</dd>
</div>
<div>
<dt>Venue</dt>
<dd>AB 7/101-104</dd>
</div>
<div>
<dt>Registration</dt>
<dd>Open until July 14</dd>
</div>
</dl>
</section>
<section class="barcamp-section barcamp-section--intro">
<div>
<p class="eyebrow">What is a Barcamp?</p>
<h2>The room makes the program.</h2>
</div>
<div class="barcamp-section__copy">
<p>
A Barcamp is an open, participatory unconference. Instead of arriving to a
fully fixed speaker list, participants help create the agenda by proposing
sessions, joining conversations, and moving toward the rooms where they can
learn or contribute the most.
</p>
<p>
Sessions can be talks, demos, teach-ins, open discussions, problem clinics,
reading circles, or small workshops. The common thread is simple: everyone
has permission to shape the day.
</p>
</div>
</section>
<section class="barcamp-card-grid" aria-label="Barcamp format">
<article>
<span aria-hidden="true">01</span>
<h3>Propose a session</h3>
<p>Share an idea, a question, a demo, a problem, or a work-in-progress before the event.</p>
</article>
<article>
<span aria-hidden="true">02</span>
<h3>Show up early</h3>
<p>Time slots open in person on the day, first come first served.</p>
</article>
<article>
<span aria-hidden="true">03</span>
<h3>Move freely</h3>
<p>Find the conversations that are useful to you, and leave room for serendipity.</p>
</article>
</section>
<section class="barcamp-section">
<div>
<p class="eyebrow">Who should come?</p>
<h2>Curious people with something to share.</h2>
</div>
<div class="barcamp-section__copy">
<p>
Students, researchers, teachers, builders, designers, writers, organizers,
artists, engineers, and curious local participants are all welcome. The day
is strongest when the room has different kinds of expertise and different
kinds of questions.
</p>
<div class="barcamp-topic-strip" aria-label="Possible session themes">
<span>computing</span>
<span>education</span>
<span>games</span>
<span>math</span>
<span>design</span>
<span>civic tech</span>
<span>science</span>
<span>culture</span>
</div>
</div>
</section>
<section class="barcamp-cta">
<div>
<p class="eyebrow">Registration</p>
<h2>Let us know you are coming.</h2>
<p>
Registration helps us plan lunch, room capacity, campus entry, and the
participant updates for the day.
</p>
</div>
<a class="button button--barcamp" href={barcampRegistrationUrl}>Register now</a>
</section>
</div>
)}
{pageEntry.id === "participants" && (
<div class="event-shell event-shell--single barcamp-shell">
<section class="barcamp-participants" aria-label="Participants">
<div class="barcamp-participant-grid">
{barcampParticipants.map((person) => (
<article class="barcamp-participant">
<h3>{person.name}</h3>
</article>
))}
</div>
</section>
</div>
)}
{pageEntry.id === "schedule" && (
<div class="event-shell event-shell--single barcamp-shell">
<section
class="barcamp-schedule"
data-barcamp-schedule
>
<div class="barcamp-schedule__head">
<div>
<p class="eyebrow">Schedule</p>
<h2>Propose now. Pick slots on the day.</h2>
</div>
<p>
Add session ideas to the shared proposal document before the event.
Time slots are assigned in person on the room whiteboard, first come
first served.
</p>
</div>
<div class="barcamp-proposal-callout">
<div>
<h3>Have something you might want to run?</h3>
<p>
Put the topic and a short high-level description in the shared document
so other participants can read it before they arrive. This is not a
time-slot booking; the schedule stays intentionally fluid until the day.
</p>
</div>
<a
class="button button--barcamp"
href={barcampProposalDocUrl}
target="_blank"
rel="noreferrer"
aria-label="Open the Barcamp 2026 session proposal Google Doc"
>
Propose a session
</a>
</div>
<div class="barcamp-tablist" role="tablist" aria-label="Schedule tracks">
{barcampTracks.map((track, index) => (
<button
type="button"
role="tab"
id={`tab-${track.id}`}
aria-controls={`panel-${track.id}`}
aria-selected={index === 0 ? "true" : "false"}
tabindex={index === 0 ? "0" : "-1"}
data-track-tab={track.id}
aria-label={`${track.label}, room ${track.room}`}
>
<span class="barcamp-tab-title">{track.label}</span>
<span class="barcamp-tab-room">Room {track.room}</span>
</button>
))}
</div>
{barcampTracks.map((track, index) => (
<section
class="barcamp-track-panel"
id={`panel-${track.id}`}
role="tabpanel"
tabindex="0"
aria-labelledby={`tab-${track.id}`}
data-track-panel={track.id}
hidden={index !== 0}
>
<div class="barcamp-track-panel__intro">
<div class="barcamp-track-panel__title">
<h3>{track.label} <span>Room {track.room}</span></h3>
</div>
<p>{track.description}</p>
</div>
{track.id === "workshops" ? (
<div class="barcamp-workshops">
<h3>Coming soon</h3>
<p>
Workshop and birds-of-a-feather sessions will be listed here once
the workshop board opens. Watercooler space is in 7/104.
</p>
</div>
) : (
<div class="barcamp-slot-groups">
{trackScheduleGroups[track.id].map((group) => (
<section class="barcamp-slot-group" aria-labelledby={`${track.id}-${group.id}-heading`}>
<div class="barcamp-slot-group__head">
<h4 id={`${track.id}-${group.id}-heading`}>{group.label}</h4>
{group.note && <p>{group.note}</p>}
</div>
<ol class="barcamp-slot-list">
{group.items.map((item) => (
<li
class:list={["barcamp-slot", item.kind === "fixed" && "barcamp-slot--fixed"]}
>
<time>{item.time}</time>
<div class="barcamp-slot__body">
<h5>{item.title}</h5>
{item.kind === "fixed" && "note" in item && item.note && <p>{item.note}</p>}
</div>
</li>
))}
</ol>
</section>
))}
</div>
)}
</section>
))}
</section>
</div>
)}
{pageEntry.id === "schedule" && (
<script is:inline>
const scheduleRoot = document.querySelector("[data-barcamp-schedule]");
if (scheduleRoot) {
const tabs = Array.from(scheduleRoot.querySelectorAll("[data-track-tab]"));
const panels = Array.from(scheduleRoot.querySelectorAll("[data-track-panel]"));
const activateTrack = (trackId) => {
tabs.forEach((tab) => {
const isActive = tab.getAttribute("data-track-tab") === trackId;
tab.setAttribute("aria-selected", isActive ? "true" : "false");
tab.setAttribute("tabindex", isActive ? "0" : "-1");
});
panels.forEach((panel) => {
panel.hidden = panel.getAttribute("data-track-panel") !== trackId;
});
};
tabs.forEach((tab, index) => {
tab.addEventListener("click", () => {
activateTrack(tab.getAttribute("data-track-tab"));
});
tab.addEventListener("keydown", (event) => {
const keyMap = {
ArrowRight: 1,
ArrowLeft: -1,
};
if (event.key === "Home" || event.key === "End" || event.key in keyMap) {
event.preventDefault();
const nextIndex =
event.key === "Home"
? 0
: event.key === "End"
? tabs.length - 1
: (index + keyMap[event.key] + tabs.length) % tabs.length;
const nextTab = tabs[nextIndex];
nextTab.focus();
activateTrack(nextTab.getAttribute("data-track-tab"));
}
});
});
}
</script>
)}
</SiteLayout>

View file

@ -0,0 +1,78 @@
---
import type { Event } from "../data/events";
import { getEventPath, isExternalEvent } from "../data/events";
type Props = {
event: Event;
compact?: boolean;
};
const { event, compact = false } = Astro.props;
const path = getEventPath(event);
const hasDestination = path !== "#";
const external = hasDestination && isExternalEvent(event);
const kindTheme: Record<Event["kind"], NonNullable<Event["theme"]>> = {
Workshop: "sage",
Conference: "sky",
School: "violet",
Event: "rose",
Course: "amber",
Unconference: "barcamp",
};
const theme = event.highlight ? event.theme ?? kindTheme[event.kind] : kindTheme[event.kind];
const title = event.shortTitle ?? event.title;
const actionLabel = external ? `Open event website for ${title}` : `Open details for ${title}`;
---
<article
class:list={["event-card", `event-card--${theme}`, event.highlight && "event-card--highlight", compact && "event-card--compact"]}
data-event-card
data-event-year={event.year}
data-event-kind={event.kind}
>
<div class="event-card__meta">
<button
class="event-badge"
type="button"
data-filter-year={event.year}
aria-pressed="false"
aria-label={`Filter catalog to year ${event.year}`}
>
{event.year}
</button>
<button
class="event-badge"
type="button"
data-filter-kind={event.kind}
aria-pressed="false"
aria-label={`Filter catalog to type ${event.kind}`}
>
{event.kind}
</button>
{event.status && <span class="event-badge">{event.status}</span>}
</div>
<h3>
{hasDestination ? (
<a href={path} target={external ? "_blank" : undefined} rel={external ? "noreferrer" : undefined}>
{title}
</a>
) : (
title
)}
</h3>
<p>{event.summary}</p>
<div class="event-card__footer">
<span>{event.date}</span>
{hasDestination && (
<a
class="text-link"
href={path}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
aria-label={actionLabel}
>
{external ? "Event website" : "Details"}
</a>
)}
</div>
</article>

View file

@ -0,0 +1,259 @@
---
import type { Event, EventPageEntry } from "../data/events";
import {
getEventBasePath,
getEventOverviewSectionIndex,
getEventPageEntries,
} from "../data/events";
import EventSchedule from "./EventSchedule.astro";
import { getEventNavItems } from "../data/eventNav";
import SiteLayout from "../layouts/SiteLayout.astro";
type Props = {
event: Event;
pageEntry?: EventPageEntry;
};
const { event } = Astro.props;
const eventPages = getEventPageEntries(event);
const pageEntry = Astro.props.pageEntry ?? eventPages[0];
const theme = event.theme ?? "default";
const basePath = getEventBasePath(event);
const overviewSectionIndex = getEventOverviewSectionIndex(event);
const overviewSection =
typeof overviewSectionIndex === "number" ? event.sections?.[overviewSectionIndex] : undefined;
const overviewSections = overviewSection ? [overviewSection] : [];
const currentSection =
pageEntry.type === "section" && typeof pageEntry.sectionIndex === "number"
? event.sections?.[pageEntry.sectionIndex]
: undefined;
const schedulePage = eventPages.find((page) => page.type === "schedule");
const eventTitle = event.shortTitle ?? event.title;
const isOverview = pageEntry.type === "overview";
const pageTitle = isOverview ? eventTitle : `${pageEntry.label} - ${eventTitle}`;
const pageContext = `${event.kind} / ${event.year}`;
const navItems = getEventNavItems(event);
const showOverviewContent = event.showOverviewContent !== false;
const showScheduleBlock = Boolean(
event.schedule && (pageEntry.type === "schedule" || (isOverview && event.showScheduleOnOverview)),
);
const shouldRenderBody = !isOverview || showOverviewContent || showScheduleBlock;
const shouldUseWideBody = Boolean(isOverview && event.showScheduleOnOverview);
const scheduleAsTabs =
showScheduleBlock && event.scheduleDisplay === "tabs" && (event.schedule?.length ?? 0) > 1;
const showHeroScheduleLink = Boolean(schedulePage && !event.showScheduleOnOverview);
const hasHeroActions = Boolean(event.sourceUrl || showHeroScheduleLink);
---
<SiteLayout
title={pageTitle}
description={event.summary}
theme={theme}
brandHref={basePath}
brandTitle={eventTitle}
brandSubtitle="Event home"
navItems={navItems}
>
{isOverview ? (
<section class:list={["event-hero", `event-hero--${theme}`]}>
<div class="event-hero__content">
<p class="eyebrow">{event.kind} / {event.year}</p>
<h1>{event.title}</h1>
{event.subtitle && <p class="lede">{event.subtitle}</p>}
<p>{event.summary}</p>
{hasHeroActions && (
<div class="hero-actions">
{event.sourceUrl && <a class="button button--secondary" href={event.sourceUrl}>Event website</a>}
{showHeroScheduleLink && schedulePage && <a class="button" href={schedulePage.href}>Schedule</a>}
</div>
)}
</div>
<aside class="event-hero__panel" aria-label="Event facts">
{event.image ? (
<img src={event.image.src} alt={event.image.alt} loading="eager" />
) : (
<div class="event-visual" aria-hidden="true">
<span></span><span></span><span></span><span></span>
</div>
)}
<dl>
<div>
<dt>Date</dt>
<dd>{event.date}</dd>
</div>
{event.location && (
<div>
<dt>Location</dt>
<dd>{event.location}</dd>
</div>
)}
{event.showRoleInHero !== false && (
<div>
<dt>Role</dt>
<dd>{event.role}</dd>
</div>
)}
{event.facts?.map((fact) => (
<div>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
</div>
))}
</dl>
</aside>
</section>
) : (
<section class="event-page-heading">
<nav class="event-breadcrumb" aria-label="Event breadcrumb">
<a href={basePath}>{eventTitle}</a>
<span aria-hidden="true">/</span>
<span>{pageEntry.label}</span>
</nav>
<p class="eyebrow">{pageContext}</p>
<h1>{pageEntry.label}</h1>
</section>
)}
{shouldRenderBody && (
<div class:list={["event-shell", "event-shell--single", shouldUseWideBody && "event-shell--wide"]}>
<div class="event-content">
{showScheduleBlock && event.schedule && (
<EventSchedule schedule={event.schedule} asTabs={scheduleAsTabs} />
)}
{isOverview && showOverviewContent && event.links && (
<section class="link-strip" aria-label="Related links">
{event.links.map((link) => <a href={link.href}>{link.label}</a>)}
</section>
)}
{isOverview && showOverviewContent && overviewSections.map((section) => (
<section class="content-section">
{section.eyebrow && <p class="eyebrow">{section.eyebrow}</p>}
<h2>{section.title}</h2>
{section.body?.map((paragraph) => <p>{paragraph}</p>)}
{section.numbered && (
<ol class="numbered-list">
{section.numbered.map((item) => <li>{item}</li>)}
</ol>
)}
{section.bullets && (
<ul class="check-list">
{section.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
{section.table && (
<div class="table-wrap">
<table>
<thead>
<tr>{section.table.headers.map((header) => <th>{header}</th>)}</tr>
</thead>
<tbody>
{section.table.rows.map((row) => (
<tr>{row.map((cell) => <td>{cell}</td>)}</tr>
))}
</tbody>
</table>
</div>
)}
{section.tables?.map((table) => (
<div class="table-wrap">
<table>
<thead>
<tr>{table.headers.map((header) => <th>{header}</th>)}</tr>
</thead>
<tbody>
{table.rows.map((row) => (
<tr>{row.map((cell) => <td>{cell}</td>)}</tr>
))}
</tbody>
</table>
</div>
))}
</section>
))}
{isOverview && showOverviewContent && !overviewSection && (
<section class="content-section">
<h2>Overview</h2>
<p>{event.summary}</p>
</section>
)}
{pageEntry.type === "section" && currentSection && (
<section class="content-section">
{currentSection.eyebrow && <p class="eyebrow">{currentSection.eyebrow}</p>}
<h2>{currentSection.title}</h2>
{currentSection.body?.map((paragraph) => <p>{paragraph}</p>)}
{currentSection.numbered && (
<ol class="numbered-list">
{currentSection.numbered.map((item) => <li>{item}</li>)}
</ol>
)}
{currentSection.bullets && (
<ul class="check-list">
{currentSection.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
{currentSection.table && (
<div class="table-wrap">
<table>
<thead>
<tr>{currentSection.table.headers.map((header) => <th>{header}</th>)}</tr>
</thead>
<tbody>
{currentSection.table.rows.map((row) => (
<tr>{row.map((cell) => <td>{cell}</td>)}</tr>
))}
</tbody>
</table>
</div>
)}
{currentSection.tables?.map((table) => (
<div class="table-wrap">
<table>
<thead>
<tr>{table.headers.map((header) => <th>{header}</th>)}</tr>
</thead>
<tbody>
{table.rows.map((row) => (
<tr>{row.map((cell) => <td>{cell}</td>)}</tr>
))}
</tbody>
</table>
</div>
))}
</section>
)}
{pageEntry.type === "visual" && (
<section class="content-section image-section">
<h2>Visual Material</h2>
{event.gallery ? (
<div class="gallery-grid">
{event.gallery.map((image) => (
<figure>
<img src={image.src} alt={image.alt} loading="lazy" />
<figcaption>{image.caption}</figcaption>
</figure>
))}
</div>
) : event.image && (
<img src={event.image.src} alt={event.image.alt} loading="lazy" />
)}
</section>
)}
{pageEntry.type === "people" && event.people && (
<section class="content-section">
<h2>Participants</h2>
<ul class="people-list">
{event.people.map((person) => <li>{person}</li>)}
</ul>
</section>
)}
</div>
</div>
)}
</SiteLayout>

View file

@ -0,0 +1,151 @@
---
import type { ScheduleDay } from "../data/events";
type Props = {
schedule: ScheduleDay[];
asTabs?: boolean;
};
const { schedule, asTabs = false } = Astro.props;
---
<section class="content-section">
<h2>Schedule</h2>
{asTabs ? (
<div class="schedule-tabs" data-schedule-tabs>
<div class="schedule-tablist" role="tablist" aria-label="Schedule days">
{schedule.map((day, index) => (
<button
type="button"
role="tab"
id={`schedule-tab-${index}`}
aria-controls={`schedule-panel-${index}`}
aria-selected={index === 0 ? "true" : "false"}
tabindex={index === 0 ? "0" : "-1"}
data-schedule-tab={`${index}`}
>
<span>{day.label}</span>
{day.date && <small>{day.date}</small>}
</button>
))}
</div>
<div class="schedule-grid">
{schedule.map((day, index) => (
<article
class="schedule-day"
id={`schedule-panel-${index}`}
role="tabpanel"
tabindex="0"
aria-labelledby={`schedule-tab-${index}`}
data-schedule-panel={`${index}`}
hidden={index !== 0}
>
<div class="schedule-day__head">
<h3>{day.label}</h3>
{day.date && <p>{day.date}</p>}
{day.moderator && <p>{day.moderator}</p>}
</div>
<ol>
{day.items.map((item) => (
<li class:list={["schedule-item", item.kind && `schedule-item--${item.kind}`]}>
<time>{item.time}</time>
<div>
<h4>{item.title}</h4>
{item.speaker && <p class="speaker">{item.speaker}</p>}
{item.affiliation && <p class="muted">{item.affiliation}</p>}
{item.detail && <p>{item.detail}</p>}
{item.links && (
<div class="inline-links">
{item.links.map((link) => <a href={link.href}>{link.label}</a>)}
</div>
)}
</div>
</li>
))}
</ol>
</article>
))}
</div>
</div>
) : (
<div class="schedule-grid">
{schedule.map((day) => (
<article class="schedule-day">
<div class="schedule-day__head">
<h3>{day.label}</h3>
{day.date && <p>{day.date}</p>}
{day.moderator && <p>{day.moderator}</p>}
</div>
<ol>
{day.items.map((item) => (
<li class:list={["schedule-item", item.kind && `schedule-item--${item.kind}`]}>
<time>{item.time}</time>
<div>
<h4>{item.title}</h4>
{item.speaker && <p class="speaker">{item.speaker}</p>}
{item.affiliation && <p class="muted">{item.affiliation}</p>}
{item.detail && <p>{item.detail}</p>}
{item.links && (
<div class="inline-links">
{item.links.map((link) => <a href={link.href}>{link.label}</a>)}
</div>
)}
</div>
</li>
))}
</ol>
</article>
))}
</div>
)}
</section>
{asTabs && (
<script is:inline>
const scheduleRoot = document.querySelector("[data-schedule-tabs]");
if (scheduleRoot) {
const tabs = Array.from(scheduleRoot.querySelectorAll("[data-schedule-tab]"));
const panels = Array.from(scheduleRoot.querySelectorAll("[data-schedule-panel]"));
const activateScheduleDay = (dayIndex) => {
tabs.forEach((tab) => {
const isActive = tab.getAttribute("data-schedule-tab") === dayIndex;
tab.setAttribute("aria-selected", isActive ? "true" : "false");
tab.setAttribute("tabindex", isActive ? "0" : "-1");
});
panels.forEach((panel) => {
panel.hidden = panel.getAttribute("data-schedule-panel") !== dayIndex;
});
};
tabs.forEach((tab, index) => {
tab.addEventListener("click", () => {
activateScheduleDay(tab.getAttribute("data-schedule-tab"));
});
tab.addEventListener("keydown", (event) => {
const keyMap = {
ArrowRight: 1,
ArrowLeft: -1,
};
if (event.key === "Home" || event.key === "End" || event.key in keyMap) {
event.preventDefault();
const nextIndex =
event.key === "Home"
? 0
: event.key === "End"
? tabs.length - 1
: (index + keyMap[event.key] + tabs.length) % tabs.length;
const nextTab = tabs[nextIndex];
nextTab.focus();
activateScheduleDay(nextTab.getAttribute("data-schedule-tab"));
}
});
});
}
</script>
)}

View file

@ -0,0 +1,21 @@
---
import type { InfoPage } from "../data/info";
type Props = {
page: InfoPage;
};
const { page } = Astro.props;
---
{page.sections.map((section) => (
<section class="content-section">
<h2>{section.title}</h2>
{section.body?.map((paragraph) => <p>{paragraph}</p>)}
{section.bullets && (
<ul class="check-list">
{section.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
</section>
))}

304
src/data/barcamp.ts Normal file
View file

@ -0,0 +1,304 @@
export const barcampRegistrationUrl = "https://events.neeldhara.cloud/event/2/barcamp-2026";
export const barcampProposalDocUrl =
"https://docs.google.com/document/d/1IHnPPOd2Ij9-9_31BkvqodZZSMCuc1-3ampwdfA60Ek/edit?usp=sharing";
export type BarcampParticipant = {
name: string;
themes?: string;
affiliation?: string;
status: "Confirmed" | "Likely";
};
export const barcampParticipants: BarcampParticipant[] = [
{
name: "Jyothi Krishnan",
themes: "Games and Math",
affiliation: "CCL and CSE, IITGN",
status: "Likely",
},
{
name: "Manoj Gupta",
themes: "Algorithms, Puzzles",
affiliation: "CSE, IITGN",
status: "Confirmed",
},
{
name: "Anirban Dasgupta",
themes: "Theory of ML, Scaleable ML",
affiliation: "CSE, IITGN",
status: "Confirmed",
},
{
name: "Shanmuganathan Raman",
themes: "Computer Vision, Graphics",
affiliation: "CSE, IITGN",
status: "Confirmed",
},
{
name: "Jaison Manjaly",
themes: "Philosophy, Curiosity, Art",
affiliation: "HSS, IITGN",
status: "Confirmed",
},
{
name: "Adithi Iyer",
themes: "STEM education, hands-on learning",
affiliation: "CCL, IITGN",
status: "Confirmed",
},
{
name: "Neha Garg",
themes: "STEM education, hands-on learning",
affiliation: "CCL, IITGN",
status: "Confirmed",
},
{
name: "Neal Apte",
themes: "Board Games, Puzzles",
affiliation: "CCL, IITGN",
status: "Confirmed",
},
{
name: "Rohit Narayanan",
themes: "Algorithms, Math",
affiliation: "Grad Student, CSE, IITGN",
status: "Likely",
},
{
name: "Priyanshi",
themes: "Math, Algorithms",
affiliation: "Grad Student, CSE, IITGN",
status: "Confirmed",
},
{
name: "Ajay Maor",
themes: "Textiles, personal computing",
affiliation: "Calico Museum (?)",
status: "Likely",
},
{
name: "Venkatesh Chopella",
themes: "PL, Computing Education",
affiliation: "IIITH",
status: "Likely",
},
{
name: "Rui Marinheiro",
themes: "Networks",
affiliation: "ISCTE Lisbon",
status: "Likely",
},
{
name: "Olinda Coelho Monteiro",
themes: "Biochemistry, Materials (?)",
affiliation: "University of Lisbon",
status: "Likely",
},
{
name: "Aditya Sengupta",
status: "Confirmed",
},
{
name: "Sudarshan Iyengar",
themes: "Computing Education, Agri Tech",
affiliation: "IIT Ropar",
status: "Likely",
},
{
name: "Subodh Sharma",
themes: "Formal Methods, EVMs, Privacy",
affiliation: "IIT Delhi",
status: "Likely",
},
{
name: "Aalok Thakkar",
themes: "Ancient Indian Math, PL, Lean, Education",
affiliation: "Ashoka University",
status: "Confirmed",
},
{
name: "Devdas",
themes: "The internet",
status: "Confirmed",
},
{
name: "Vinayak Hegde",
status: "Confirmed",
},
{
name: "Gaurav Chaturvedi",
status: "Likely",
},
{
name: "Ankit Daftery",
status: "Likely",
},
{
name: "Guru",
themes: "Identity, KYC, banking",
status: "Likely",
},
];
export type BarcampTrackId = "track-a" | "track-b" | "workshops";
export type BarcampTrack = {
id: BarcampTrackId;
label: string;
description: string;
room: string;
};
export const barcampTracks: BarcampTrack[] = [
{
id: "track-a",
label: "Track A",
description: "Session slots for short talks, demos, provocations, and questions.",
room: "7/102",
},
{
id: "track-b",
label: "Track B",
description: "A parallel track with the same timing pattern as Track A.",
room: "7/103",
},
{
id: "workshops",
label: "Workshops",
description: "Hands-on and birds-of-a-feather sessions will be added closer to the event.",
room: "7/101",
},
];
export type BarcampScheduleItem =
| {
kind: "fixed";
id: string;
time: string;
title: string;
note?: string;
}
| {
kind: "slot";
id: string;
time: string;
title: string;
};
export type BarcampScheduleGroup = {
id: string;
label: string;
note?: string;
items: BarcampScheduleItem[];
};
const talkBlocks = [
["10:00-10:30", "Open session slot"],
["10:30-11:00", "Open session slot"],
["11:00-11:30", "Open session slot"],
["11:30-12:00", "Open session slot"],
["12:00-12:30", "Open session slot"],
["12:30-13:00", "Open session slot"],
["14:30-15:00", "Open session slot"],
["15:00-15:30", "Open session slot"],
["15:30-16:00", "Open session slot"],
["16:30-17:00", "Open session slot"],
["17:00-17:30", "Open session slot"],
["17:30-18:00", "Open session slot"],
] as const;
export function getBarcampTrackScheduleGroups(
trackId: Exclude<BarcampTrackId, "workshops">,
) {
const withTrackId = (slotId: string) => `${trackId}-${slotId}`;
return [
{
id: "arrival",
label: "Arrival",
note: "09:30-10:00",
items: [
{
kind: "fixed",
id: withTrackId("registration"),
time: "09:30-10:00",
title: "Registration",
note: "Check in, collect your badge, and find the session board.",
},
],
},
{
id: "morning",
label: "Early Bird sessions",
note: "10:00-11:30",
items: talkBlocks.slice(0, 3).map(([time, title], index) => ({
kind: "slot" as const,
id: withTrackId(`talk-${index + 1}`),
time,
title,
})),
},
{
id: "midday",
label: "Morning sessions",
note: "11:30-13:00",
items: [
{
kind: "fixed",
id: withTrackId("morning-break"),
time: "11:30",
title: "Break and room reset",
},
...talkBlocks.slice(3, 6).map(([time, title], index) => ({
kind: "slot" as const,
id: withTrackId(`talk-${index + 4}`),
time,
title,
})),
],
},
{
id: "lunch",
label: "Lunch and CCL tour",
note: "13:00-14:30",
items: [
{
kind: "fixed",
id: withTrackId("lunch"),
time: "13:00-14:30",
title: "Lunch and CCL tour",
},
],
},
{
id: "afternoon",
label: "Afternoon sessions",
note: "14:30-16:00",
items: talkBlocks.slice(6, 9).map(([time, title], index) => ({
kind: "slot" as const,
id: withTrackId(`talk-${index + 7}`),
time,
title,
})),
},
{
id: "evening",
label: "Evening sessions",
note: "16:00-18:00",
items: [
{
kind: "fixed",
id: withTrackId("tea-break"),
time: "16:00-16:30",
title: "Break",
},
...talkBlocks.slice(9).map(([time, title], index) => ({
kind: "slot" as const,
id: withTrackId(`talk-${index + 10}`),
time,
title,
})),
],
},
] satisfies BarcampScheduleGroup[];
}

87
src/data/eventNav.ts Normal file
View file

@ -0,0 +1,87 @@
import { infoPages } from "./info";
import {
getEventBasePath,
getEventPageEntries,
type EventPageEntry,
type Event,
} from "./events";
function getSectionGroup(page: EventPageEntry) {
const planning = new Set([
"workshops",
"important-dates",
"registration-logistics",
"contact",
"registration",
"workshop-registration",
"registration-notes",
"socials-at-fsttcs-2024",
]);
const travel = new Set([
"internet-access-via-wifi",
"accommodation-for-students",
"guest-house-and-hotel-accommodation",
"fortune-inn-haveli",
"for-international-participants",
"international-visitor-notes",
"reaching-and-leaving-iit-gandhinagar",
]);
if (planning.has(page.id)) return "Planning";
if (travel.has(page.id)) return "Travel & Stay";
if (page.id !== "overview") return "Venue & Campus";
return undefined;
}
export function getEventNavItems(event: Event) {
const basePath = getEventBasePath(event);
const eventPages = getEventPageEntries(event);
const sectionPages = eventPages.filter((page) => page.type === "section");
const schedulePage = eventPages.find((page) => page.type === "schedule");
const peoplePage = eventPages.find((page) => page.type === "people");
const visualPage = eventPages.find((page) => page.type === "visual");
if (event.slug === "barcamp") {
return [
...(!event.hideOverviewNav ? [{ label: "Overview", href: basePath }] : []),
...(peoplePage ? [{ label: "Participants", href: peoplePage.href }] : []),
...(schedulePage ? [{ label: "Schedule", href: schedulePage.href }] : []),
{
label: "Venue",
items: infoPages.map((page) => ({
label: page.title,
href: `${basePath}info/${page.slug}/`,
})),
},
];
}
return [
...(!event.hideOverviewNav ? [{ label: "Overview", href: basePath }] : []),
...(sectionPages.length
? [
{
label: "Details",
items: sectionPages.map((page) => ({
label: page.label,
href: page.href,
group: sectionPages.length >= 16 ? getSectionGroup(page) : undefined,
})),
},
]
: []),
...(schedulePage ? [{ label: "Schedule", href: schedulePage.href }] : []),
...(peoplePage ? [{ label: "Participants", href: peoplePage.href }] : []),
...(visualPage ? [{ label: "Visuals", href: visualPage.href }] : []),
{
label: "Information",
items: infoPages.map((page) => ({
label: page.title,
href: `${basePath}info/${page.slug}/`,
})),
},
...(event.sourceUrl
? [{ label: "Event website", href: event.sourceUrl, external: true }]
: []),
];
}

1032
src/data/events.ts Normal file

File diff suppressed because it is too large Load diff

106
src/data/info.ts Normal file
View file

@ -0,0 +1,106 @@
export type InfoPage = {
slug: string;
title: string;
sections: {
title: string;
body?: string[];
bullets?: string[];
}[];
};
export const infoPages: InfoPage[] = [
{
slug: "reaching-iitgn",
title: "Reaching IIT Gandhinagar",
sections: [
{
title: "Getting here",
bullets: [
"Sardar Vallabhbhai Patel International Airport (AMD) serves Ahmedabad and Gandhinagar and is about a 30-minute drive from IIT Gandhinagar in normal traffic.",
"Ahmedabad Junction Railway Station (ADI), locally known as Kalupur station, is the main railway terminus and is well connected to the rest of India.",
"Geeta Mandir and Paldi are major bus terminals in Ahmedabad.",
"Gate 1 is the usual entry point for visitors unless event-specific instructions say otherwise.",
],
},
{
title: "Cabs and autos",
body: [
"App-based cabs and autos are usually available from the airport, railway station, and city. Leaving campus late at night or early in the morning benefits from advance booking.",
"For major events, reliable private transport contacts can be shared with registered participants over email or a participant messaging group.",
],
},
{
title: "On campus",
bullets: [
"Academic Block 1 is a common venue for workshops and talks.",
"The campus is walkable, but allow extra time if you are coming from the main gate, guest house, or hostels.",
"The medical center is in the Central Arcade. Regular contact: 079-2395-1116. Emergency contact: +91-7069795000.",
],
},
],
},
{
slug: "gandhinagar-ahmedabad",
title: "Gandhinagar and Ahmedabad",
sections: [
{
title: "The region",
body: [
"IIT Gandhinagar is located in Gandhinagar, close to Ahmedabad. Ahmedabad has the larger airport, railway station, hotels, restaurants, museums, old-city walks, and many intercity transit options.",
"Gandhinagar is quieter and closer to campus. Many events use either on-campus accommodation or a Gandhinagar hotel with organized shuttles.",
],
},
{
title: "Campus areas",
bullets: [
"Residential area: staff and faculty housing.",
"Student hostels: mess, coffee and juice shops, groceries, and informal gathering spaces.",
"Academic area: classrooms, offices, labs, library, and common event venues.",
"Sports complex: swimming, climbing wall, squash, badminton, table tennis, football, cricket, running track, gym, and a juice shop.",
"Central Arcade: bank, ATM, post office, medical center, dry cleaning, and local shops.",
],
},
{
title: "Food and daily needs",
body: [
"Gujarat is predominantly vegetarian, and food can be spicy. Event meals usually include milder options when organizers know participant needs in advance.",
"Most campus vendors accept digital payments, but visitors should keep a little cash for exceptions.",
],
},
],
},
{
slug: "international-participants",
title: "International Participants",
sections: [
{
title: "Visa documents",
body: [
"Foreign nationals entering India generally need a valid passport and an appropriate Indian visa or eVisa category. Conference participants should check whether a conference visa is required for their event.",
"If an invitation letter or government clearance letter is required, event organizers will usually collect participant details well in advance. Follow the event-specific deadline.",
],
},
{
title: "Money and connectivity",
bullets: [
"The currency is the Indian Rupee (INR). Currency exchange is available at the airport and through local banks.",
"UPI QR-code payments are common, but international visitors should keep cards enabled for international transactions and carry some cash.",
"Eduroam works on campus. Events may also provide temporary IITGN WiFi credentials.",
"A local or roaming cellular data connection is useful for cabs, maps, and off-campus movement.",
],
},
{
title: "Practical information",
bullets: [
"India uses 230V, 50Hz power. Type C plugs are common; bring an adapter if needed.",
"Most signage in Ahmedabad, Gandhinagar, and IITGN is available in English, and language barriers are usually manageable.",
"Share accessibility, dietary, and accommodation needs with organizers early.",
],
},
],
},
];
export function getInfoPage(slug: string) {
return infoPages.find((page) => page.slug === slug);
}

1
src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View file

@ -0,0 +1,191 @@
---
import "../styles/global.css";
type NavLink = {
label: string;
href: string;
description?: string;
external?: boolean;
group?: string;
};
type NavItem =
| NavLink
| {
label: string;
items: NavLink[];
};
type Props = {
title?: string;
description?: string;
theme?: string;
brandHref?: string;
brandTitle?: string;
brandSubtitle?: string;
navItems?: NavItem[];
};
const {
title = "Events",
description = "Events organized by Neeldhara Misra.",
theme = "default",
brandHref = "/",
brandTitle = "Events",
brandSubtitle = "organized events and local guides",
navItems: suppliedNavItems,
} = Astro.props;
const pageTitle = title === "Events" ? "Events" : `${title} - Events`;
const defaultNavItems: NavItem[] = [
{ label: "Organized", href: "/" },
{ label: "Attended", href: "/attended/" },
];
const navItems = suppliedNavItems ?? defaultNavItems;
const navVariant = suppliedNavItems ? "standard" : "toggle";
const pathname = Astro.url.pathname;
const isMenu = (item: NavItem): item is Extract<NavItem, { items: NavLink[] }> => "items" in item;
const isCurrent = (href: string) => pathname === href;
const getMenuGroups = (items: NavLink[]) => {
const groups: { label?: string; items: NavLink[] }[] = [];
items.forEach((child) => {
const groupLabel = child.group;
let group = groups.find((candidate) => candidate.label === groupLabel);
if (!group) {
group = { label: groupLabel, items: [] };
groups.push(group);
}
group.items.push(child);
});
return groups;
};
const getMenuLayout = (item: Extract<NavItem, { items: NavLink[] }>) => {
const itemCount = item.items.length;
const hasDescriptions = item.items.some((child) => child.description);
const groups = getMenuGroups(item.items);
const hasGroups = groups.some((group) => group.label);
const columns = hasGroups ? Math.min(groups.length, 3) : itemCount >= 16 ? 3 : itemCount >= 7 ? 2 : 1;
const width = itemCount >= 16 ? "58rem" : columns === 2 ? "38rem" : hasDescriptions ? "26rem" : "18rem";
const density = itemCount >= 10 ? "compact" : hasDescriptions ? "descriptive" : "simple";
return {
columns,
width,
density,
groups,
hasGroups,
};
};
---
<!doctype html>
<html lang="en" data-theme={theme}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<title>{pageTitle}</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
</head>
<body>
<a class="skip-link" href="#content">Skip to content</a>
<header class="site-header">
<div class="site-header__inner">
<a class="brand" href={brandHref} aria-label={`${brandTitle} home`}>
<span class="brand__mark" aria-hidden="true">E</span>
<span>
<strong>{brandTitle}</strong>
{brandSubtitle && <small>{brandSubtitle}</small>}
</span>
</a>
<nav class:list={["site-nav", navVariant === "toggle" && "site-nav--toggle"]} aria-label="Main navigation">
{
navItems.map((item) =>
isMenu(item) ? (() => {
const menuLayout = getMenuLayout(item);
return (
<details
class="nav-menu"
data-active={item.items.some((child) => isCurrent(child.href)) ? "true" : undefined}
data-density={menuLayout.density}
style={`--nav-menu-columns: ${menuLayout.columns}; --nav-menu-width: ${menuLayout.width};`}
>
<summary>{item.label}</summary>
<div class="nav-menu__panel" data-grouped={menuLayout.hasGroups ? "true" : undefined}>
{menuLayout.hasGroups ? (
menuLayout.groups.map((group) => (
<div class="nav-menu__group">
{group.label && <p class="nav-menu__group-label">{group.label}</p>}
{group.items.map((child) => (
<a
href={child.href}
target={child.external ? "_blank" : undefined}
rel={child.external ? "noreferrer" : undefined}
aria-current={isCurrent(child.href) ? "page" : undefined}
aria-label={child.external ? `${child.label} (external website)` : undefined}
>
<span>
{child.label}
{child.external && <span class="external-mark" aria-hidden="true">↗</span>}
</span>
{child.description && <small>{child.description}</small>}
</a>
))}
</div>
))
) : (
item.items.map((child) => (
<a
href={child.href}
target={child.external ? "_blank" : undefined}
rel={child.external ? "noreferrer" : undefined}
aria-current={isCurrent(child.href) ? "page" : undefined}
aria-label={child.external ? `${child.label} (external website)` : undefined}
>
<span>
{child.label}
{child.external && <span class="external-mark" aria-hidden="true">↗</span>}
</span>
{child.description && <small>{child.description}</small>}
</a>
))
)}
</div>
</details>
);
})() : (
<a
href={item.href}
target={item.external ? "_blank" : undefined}
rel={item.external ? "noreferrer" : undefined}
aria-current={isCurrent(item.href) ? "page" : undefined}
aria-label={item.external ? `${item.label} (external website)` : undefined}
>
{item.label}
{item.external && <span class="external-mark" aria-hidden="true">↗</span>}
</a>
)
)
}
</nav>
</div>
</header>
<main id="content">
<slot />
</main>
<footer class="site-footer">
<a class="site-footer__home" href="/">
<strong>Events</strong>
<p>Go back to all events</p>
</a>
<div class="site-footer__links">
<a href="https://www.neeldhara.com">Neeldhara Misra</a>
<a href="https://github.com/neeldhara">GitHub</a>
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,22 @@
---
import BarcampPage from "../../components/BarcampPage.astro";
import EventPage from "../../components/EventPage.astro";
import { getEventByParams, getEventPageEntry, internalEvents } from "../../data/events";
export function getStaticPaths() {
return internalEvents.map((event) => ({
params: { year: event.year, slug: event.slug },
}));
}
const { year, slug } = Astro.params;
const event = getEventByParams(year!, slug!);
if (!event) {
return Astro.redirect("/");
}
const pageEntry = getEventPageEntry(event);
---
{event.slug === "barcamp" ? <BarcampPage event={event} pageEntry={pageEntry} /> : <EventPage event={event} pageEntry={pageEntry} />}

View file

@ -0,0 +1,33 @@
---
import BarcampPage from "../../../components/BarcampPage.astro";
import EventPage from "../../../components/EventPage.astro";
import { getEventByParams, getEventPageEntries, internalEvents } from "../../../data/events";
export function getStaticPaths() {
return internalEvents.flatMap((event) =>
getEventPageEntries(event)
.filter((pageEntry) => pageEntry.type !== "overview")
.map((pageEntry) => ({
params: {
year: event.year,
slug: event.slug,
page: pageEntry.id,
},
props: {
event,
pageEntry,
},
})),
);
}
const { event, pageEntry } = Astro.props;
if (!event || !pageEntry) {
const { year, slug } = Astro.params;
const fallbackEvent = getEventByParams(year!, slug!);
if (!fallbackEvent) return Astro.redirect("/");
}
---
{event.slug === "barcamp" ? <BarcampPage event={event} pageEntry={pageEntry} /> : <EventPage event={event} pageEntry={pageEntry} />}

View file

@ -0,0 +1,68 @@
---
import InfoPageSections from "../../../../components/InfoPageSections.astro";
import SiteLayout from "../../../../layouts/SiteLayout.astro";
import { getEventNavItems } from "../../../../data/eventNav";
import {
getEventBasePath,
getEventByParams,
internalEvents,
} from "../../../../data/events";
import { getInfoPage, infoPages } from "../../../../data/info";
export function getStaticPaths() {
return internalEvents.flatMap((event) =>
infoPages.map((page) => ({
params: {
year: event.year,
slug: event.slug,
infoSlug: page.slug,
},
props: {
event,
page,
},
})),
);
}
const { event, page } = Astro.props;
if (!event || !page) {
const { year, slug, infoSlug } = Astro.params;
const fallbackEvent = getEventByParams(year!, slug!);
const fallbackPage = getInfoPage(infoSlug!);
if (!fallbackEvent || !fallbackPage) return Astro.redirect("/");
}
const eventTitle = event.shortTitle ?? event.title;
const basePath = getEventBasePath(event);
const infoNavLabel = event.slug === "barcamp" ? "Venue" : "Information";
---
<SiteLayout
title={`${page.title} - ${eventTitle}`}
description={page.title}
theme={event.theme ?? "default"}
brandHref={basePath}
brandTitle={eventTitle}
brandSubtitle="Event home"
navItems={getEventNavItems(event)}
>
<section class="event-page-heading">
<nav class="event-breadcrumb" aria-label="Event breadcrumb">
<a href={basePath}>{eventTitle}</a>
<span aria-hidden="true">/</span>
<span>{infoNavLabel}</span>
<span aria-hidden="true">/</span>
<span>{page.title}</span>
</nav>
<p class="eyebrow">{infoNavLabel}</p>
<h1>{page.title}</h1>
</section>
<div class="event-shell event-shell--single">
<div class="event-content">
<InfoPageSections page={page} />
</div>
</div>
</SiteLayout>

9
src/pages/attended.astro Normal file
View file

@ -0,0 +1,9 @@
---
import SiteLayout from "../layouts/SiteLayout.astro";
---
<SiteLayout title="Attended" description="Events attended." brandSubtitle="">
<section class="simple-hero">
<h1>Coming soon.</h1>
</section>
</SiteLayout>

3
src/pages/blog.astro Normal file
View file

@ -0,0 +1,3 @@
---
return Astro.redirect("/attended/");
---

143
src/pages/index.astro Normal file
View file

@ -0,0 +1,143 @@
---
import EventCard from "../components/EventCard.astro";
import SiteLayout from "../layouts/SiteLayout.astro";
import { getEventPath, organizedEvents } from "../data/events";
const featured = organizedEvents.find((event) => event.highlight);
const primaryEvents = organizedEvents.filter((event) => event !== featured);
const featuredPath = featured ? getEventPath(featured) : undefined;
---
<SiteLayout title="Events" description="Events organized by Neeldhara Misra." brandSubtitle="">
{featured && (
<section class="featured-band" aria-labelledby="featured-title">
<div>
<p class="eyebrow">Up next</p>
<h2 id="featured-title">{featured.title}</h2>
<p>{featured.summary}</p>
</div>
<a class="icon-link" href={featuredPath} aria-label={`Open ${featured.title} page`}>
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M10.5 13.5a3.2 3.2 0 0 0 4.5 0l3.4-3.4a3.2 3.2 0 0 0-4.5-4.5l-1.1 1.1" />
<path d="M13.5 10.5a3.2 3.2 0 0 0-4.5 0l-3.4 3.4a3.2 3.2 0 0 0 4.5 4.5l1.1-1.1" />
<path d="m9.5 14.5 5-5" />
</svg>
</a>
</section>
)}
<section class="event-list-section" id="events" data-event-filters>
<div class="filter-panel" aria-label="Filter event catalog">
<div class="filter-status">
<p class="filter-summary" aria-live="polite" data-filter-summary>Showing all {primaryEvents.length} events</p>
<button class="filter-reset" type="button" data-filter-reset hidden>Reset filters</button>
</div>
</div>
<div class="event-grid">
{primaryEvents.map((event) => <EventCard event={event} />)}
</div>
<p class="filter-empty" data-filter-empty hidden>No events match these filters.</p>
</section>
<script>
const filterRoot = document.querySelector<HTMLElement>("[data-event-filters]");
if (filterRoot) {
const cards = Array.from(filterRoot.querySelectorAll<HTMLElement>("[data-event-card]"));
const kindButtons = Array.from(filterRoot.querySelectorAll<HTMLButtonElement>("[data-filter-kind]"));
const yearButtons = Array.from(filterRoot.querySelectorAll<HTMLButtonElement>("[data-filter-year]"));
const summary = filterRoot.querySelector<HTMLElement>("[data-filter-summary]");
const empty = filterRoot.querySelector<HTMLElement>("[data-filter-empty]");
const resetButton = filterRoot.querySelector<HTMLButtonElement>("[data-filter-reset]");
const validKinds = new Set(["all", ...kindButtons.map((button) => button.dataset.filterKind).filter(Boolean)]);
const validYears = new Set(["all", ...yearButtons.map((button) => button.dataset.filterYear).filter(Boolean)]);
const params = new URLSearchParams(window.location.search);
let activeKind = "all";
let activeYear = "all";
const requestedKind = params.get("type");
const requestedYear = params.get("year");
if (requestedKind && validKinds.has(requestedKind)) activeKind = requestedKind;
if (requestedYear && validYears.has(requestedYear)) activeYear = requestedYear;
function syncButtons(
buttons: HTMLButtonElement[],
key: "filterKind" | "filterYear",
value: string,
) {
buttons.forEach((button) => {
button.setAttribute("aria-pressed", button.dataset[key] === value ? "true" : "false");
});
}
function syncUrl() {
const url = new URL(window.location.href);
if (activeKind === "all") url.searchParams.delete("type");
else url.searchParams.set("type", activeKind);
if (activeYear === "all") url.searchParams.delete("year");
else url.searchParams.set("year", activeYear);
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
}
function getActiveFilterLabel() {
const filters = [];
if (activeKind !== "all") filters.push(activeKind);
if (activeYear !== "all") filters.push(activeYear);
return filters.length ? ` for ${filters.join(" / ")}` : "";
}
function applyFilters(updateUrl = false) {
let visibleCount = 0;
cards.forEach((card) => {
const matchesKind = activeKind === "all" || card.dataset.eventKind === activeKind;
const matchesYear = activeYear === "all" || card.dataset.eventYear === activeYear;
const visible = matchesKind && matchesYear;
card.hidden = !visible;
if (visible) visibleCount += 1;
});
syncButtons(kindButtons, "filterKind", activeKind);
syncButtons(yearButtons, "filterYear", activeYear);
if (summary) {
summary.textContent =
activeKind === "all" && activeYear === "all"
? `Showing all ${cards.length} events`
: `Showing ${visibleCount} of ${cards.length} events${getActiveFilterLabel()}`;
}
if (resetButton) resetButton.hidden = activeKind === "all" && activeYear === "all";
if (empty) empty.hidden = visibleCount !== 0;
if (updateUrl) syncUrl();
}
kindButtons.forEach((button) => {
button.addEventListener("click", () => {
activeKind = button.dataset.filterKind ?? "all";
applyFilters(true);
});
});
yearButtons.forEach((button) => {
button.addEventListener("click", () => {
activeYear = button.dataset.filterYear ?? "all";
applyFilters(true);
});
});
resetButton?.addEventListener("click", () => {
activeKind = "all";
activeYear = "all";
applyFilters(true);
});
applyFilters();
}
</script>
</SiteLayout>

View file

@ -0,0 +1,27 @@
---
import InfoPageSections from "../../components/InfoPageSections.astro";
import SiteLayout from "../../layouts/SiteLayout.astro";
import { getInfoPage, infoPages } from "../../data/info";
export function getStaticPaths() {
return infoPages.map((page) => ({
params: { slug: page.slug },
}));
}
const page = getInfoPage(Astro.params.slug!);
if (!page) {
return Astro.redirect("/");
}
---
<SiteLayout title={page.title} description={page.title}>
<section class="simple-hero simple-hero--info">
<p class="eyebrow">Shared information</p>
<h1>{page.title}</h1>
</section>
<div class="info-grid">
<InfoPageSections page={page} />
</div>
</SiteLayout>

2011
src/styles/global.css Normal file

File diff suppressed because it is too large Load diff

9
tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}