Initial events Astro site
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
18
README.md
Normal 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
|
|
@ -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
|
|
@ -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
19
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/assets/events/cricket-2024-poster.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
public/assets/events/csed-2022-acm-logo.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/assets/events/csed-2022-nptel.jpg
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
public/assets/events/csed-2022-poster.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/assets/events/fsttcs-2024-map.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/assets/events/fsttcs-2024-sponsors.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
5
public/favicon.svg
Normal 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 |
BIN
public/fonts/heliotrope/heliotrope_3_bold.woff2
Normal file
BIN
public/fonts/heliotrope/heliotrope_3_italic.woff2
Normal file
BIN
public/fonts/heliotrope/heliotrope_3_regular.woff2
Normal file
BIN
public/fonts/imprima/imprima-regular.ttf
Normal file
BIN
public/fonts/playwrite-us-modern/playwrite-us-modern-200.woff2
Normal file
BIN
public/fonts/playwrite-us-modern/playwrite-us-modern-300.woff2
Normal file
BIN
public/fonts/playwrite-us-modern/playwrite-us-modern-400.woff2
Normal file
BIN
public/fonts/yuyu/yuyu-regular.ttf
Normal file
358
src/components/BarcampPage.astro
Normal 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>
|
||||||
78
src/components/EventCard.astro
Normal 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>
|
||||||
259
src/components/EventPage.astro
Normal 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>
|
||||||
151
src/components/EventSchedule.astro
Normal 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>
|
||||||
|
)}
|
||||||
21
src/components/InfoPageSections.astro
Normal 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
|
|
@ -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
|
|
@ -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
106
src/data/info.ts
Normal 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
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
191
src/layouts/SiteLayout.astro
Normal 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>
|
||||||
22
src/pages/[year]/[slug].astro
Normal 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} />}
|
||||||
33
src/pages/[year]/[slug]/[page].astro
Normal 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} />}
|
||||||
68
src/pages/[year]/[slug]/info/[infoSlug].astro
Normal 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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
return Astro.redirect("/attended/");
|
||||||
|
---
|
||||||
143
src/pages/index.astro
Normal 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>
|
||||||
27
src/pages/info/[slug].astro
Normal 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
9
tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||