From ee001df890dbe015c3e32e1b363fa70da94a85ad Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Mon, 16 Feb 2026 10:43:31 -0800 Subject: [PATCH 01/14] Draft of a new calendar UI --- AGENTS.md | 213 ++++++++++++++ src/components/EventCalendar.astro | 443 +++++++++++++++++++++++++++++ src/consts.ts | 3 + src/pages/index.astro | 14 +- 4 files changed, 661 insertions(+), 12 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/components/EventCalendar.astro diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..208be5f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,213 @@ +# AGENTS.md - Coding Agent Guidelines for VanPOP Website + +## Project Overview + +Static website for VanPOP (Vancouver) built with **Astro v5**, **TypeScript (strict)**, and **Tailwind CSS v4**. Deployed to Cloudflare Pages. Uses **pnpm** as the package manager. + +No client-side framework (React/Vue/Svelte) -- all components are `.astro` single-file components. Blog content is driven by Astro Content Collections using Markdown files with Zod-validated frontmatter. + +## Build / Lint / Test Commands + +| Command | Description | +| -------------- | ----------------------------------------------------- | +| `pnpm dev` | Start dev server (accessible on network via `--host`) | +| `pnpm start` | Start dev server (localhost only) | +| `pnpm build` | Type-check (`astro check`) then build static site | +| `pnpm preview` | Preview the production build locally | + +### Type Checking + +```sh +pnpm astro check +``` + +This is the primary code validation tool. It runs TypeScript checking for both `.ts` and `.astro` files. It is automatically run as part of `pnpm build`. + +### Linting + +- **No ESLint** is configured. Do not add ESLint rules or dependencies. +- **Markdown linting** is handled in CI via `markdownlint-cli2`. Config in `.markdownlint.json` disables line length limits (MD013), inline HTML (MD033), and duplicate headings (MD024). + +### Testing + +There is **no test framework** configured (no vitest, jest, playwright, etc.). There are no test files in the repository. Do not create test infrastructure unless explicitly asked. Validation is done via `astro check` and CI build verification. + +## Code Style + +### Formatting (Prettier) + +Configured in `.prettierrc.mjs`: + +- **Indentation**: 4 spaces (also in `.editorconfig`) +- **Semicolons**: Always (`semi: true`) +- **Quotes**: Single quotes (`singleQuote: true`) +- **Trailing commas**: ES5 style (`trailingComma: 'es5'`) + +### TypeScript + +- Extends `astro/tsconfigs/strict` with `strictNullChecks: true` +- No path aliases -- use relative imports (`../components/Foo.astro`) +- Use `import type` for type-only imports: + ```ts + import type { CollectionEntry } from 'astro:content'; + ``` + +### Imports + +Order imports as follows (no enforced linter rule, but follow existing convention): + +1. Astro built-ins (`astro:content`, `astro:transitions`) +2. Third-party packages (`@astrojs/rss`, etc.) +3. Local components (relative paths) +4. Types (using `import type`) + +Example: + +```ts +import { getCollection } from 'astro:content'; +import BaseHead from '../components/BaseHead.astro'; +import Header from '../components/Header.astro'; +import type { CollectionEntry } from 'astro:content'; +``` + +### Naming Conventions + +| Element | Convention | Example | +| ------------------ | --------------------- | ------------------------------------------- | +| Components | PascalCase `.astro` | `BlogPostsPreviewList.astro` | +| Layouts | PascalCase `.astro` | `BlogPost.astro` | +| Pages | kebab-case `.astro` | `get-involved.astro` | +| Blog posts | `YYYYMMDD-slug.md` | `20260213-finding-your-farm.md` | +| Exported constants | UPPER_SNAKE_CASE | `SITE_TITLE`, `INDEX_PAGE_BLOG_POSTS_LIMIT` | +| Variables | camelCase | `postsLimit`, `maxTitleLength` | +| CSS custom props | `--color-vp-*` prefix | `--color-vp-purple` | + +### Astro Component Structure + +Follow the standard Astro single-file component pattern: + +```astro +--- +// 1. Imports +import Footer from '../components/Footer.astro'; +import type { HTMLAttributes } from 'astro/types'; + +// 2. Props interface +interface Props { + title: string; + description?: string; +} + +// 3. Destructure props and component logic +const { title, description } = Astro.props; +--- + + +
+

{title}

+ +
+ + + + + + +``` + +### Types + +- Define component props using `interface Props` in the frontmatter +- Use Zod schemas for content collection validation (see `src/content/config.ts`) +- Use literal union types for constrained props: + ```ts + interface Props { + target?: '_blank' | '_parent' | '_top' | '_self'; + } + ``` + +### Styling + +- **Primary method**: Tailwind CSS utility classes directly in templates +- **Theme colors**: Defined in `src/styles/global.css` using Tailwind v4 `@theme` directive (prefixed `vp-*`) +- **Custom utilities**: Defined via `@layer utilities` in `global.css` +- **Scoped styles**: Use sparingly in components, only when Tailwind is insufficient +- **Blog layout**: Uses ` + + From d5206769b050e6bc779fb1965e6a101e916a512f Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Mon, 16 Feb 2026 17:36:39 -0800 Subject: [PATCH 13/14] Refactor modal code --- src/components/EventCalendar.astro | 142 +------------------------- src/components/EventDetailModal.astro | 53 ++++++++++ src/scripts/event-modal.ts | 100 ++++++++++++++++++ 3 files changed, 156 insertions(+), 139 deletions(-) create mode 100644 src/components/EventDetailModal.astro create mode 100644 src/scripts/event-modal.ts diff --git a/src/components/EventCalendar.astro b/src/components/EventCalendar.astro index 2ddf95a..355b004 100644 --- a/src/components/EventCalendar.astro +++ b/src/components/EventCalendar.astro @@ -3,6 +3,7 @@ import { GOOGLE_CALENDAR_ID } from '../consts'; import { fetchCalendarEvents } from '../lib/google-calendar'; import EventCalendarNav from './EventCalendarNav.astro'; import EventCardTemplate from './EventCardTemplate.astro'; +import EventDetailModal from './EventDetailModal.astro'; const { events, fetchError } = await fetchCalendarEvents( GOOGLE_CALENDAR_ID, @@ -20,7 +21,6 @@ const buildYear = now.getFullYear(); data-build-month={buildMonth} data-build-year={buildYear} > - diff --git a/src/scripts/event-modal.ts b/src/scripts/event-modal.ts new file mode 100644 index 0000000..4b63692 --- /dev/null +++ b/src/scripts/event-modal.ts @@ -0,0 +1,100 @@ +import type { CalendarEvent } from '../types/calendar'; +import { + MONTH_NAMES, + DAY_NAMES, + formatTime, + formatDayDate, + isAllDay, + getEventStartDate, + getEventEndDate, + renderFullLocation, +} from './calendar-utils'; + +const calOverlay = document.getElementById('cal-overlay'); +const calModal = document.getElementById('cal-modal'); + +export function openEventModal(event: CalendarEvent): void { + if (!calModal || !calOverlay) return; + + const startStr = getEventStartDate(event); + const endStr = getEventEndDate(event); + const { dayName, dayNum, monthShort } = formatDayDate(startStr); + + // Populate date badge + const monthShortEl = document.getElementById('modal-month-short'); + const dayNumEl = document.getElementById('modal-day-num'); + const dayNameEl = document.getElementById('modal-day-name'); + if (monthShortEl) monthShortEl.textContent = monthShort; + if (dayNumEl) dayNumEl.textContent = String(dayNum); + if (dayNameEl) dayNameEl.textContent = dayName.slice(0, 3); + + // Title + const titleEl = document.getElementById('modal-title'); + if (titleEl) titleEl.textContent = event.summary || 'Untitled Event'; + + // Time -- include full date for context + const timeEl = document.getElementById('modal-time'); + if (timeEl) { + const startDate = new Date(startStr); + const dateLabel = `${DAY_NAMES[startDate.getDay()]}, ${MONTH_NAMES[startDate.getMonth()]} ${startDate.getDate()}, ${startDate.getFullYear()}`; + if (isAllDay(event)) { + timeEl.textContent = `${dateLabel} \u2022 All Day`; + } else { + timeEl.textContent = `${dateLabel} \u2022 ${formatTime(startStr)} - ${formatTime(endStr)}`; + } + } + + // Location (full, not shortened) + renderFullLocation( + document.getElementById('modal-location-wrapper') as HTMLElement, + document.getElementById('modal-location') as HTMLElement, + event.location || '' + ); + + // Description (rendered as HTML) + const descWrapper = document.getElementById('modal-description-wrapper'); + const descEl = document.getElementById('modal-description'); + const rawDescription = event.description || ''; + if (descEl && descWrapper) { + if (rawDescription.trim()) { + descWrapper.classList.remove('hidden'); + descEl.innerHTML = rawDescription; + + // Style dynamically injected links from calendar description + descEl.querySelectorAll('a').forEach((a) => { + a.classList.add( + 'text-vp-purple', + 'underline', + 'hover:opacity-80' + ); + }); + } else { + descWrapper.classList.add('hidden'); + } + } + + // Show modal and overlay + calOverlay.classList.remove('hidden'); + calOverlay.classList.add('flex'); + calModal.classList.remove('hidden'); + calModal.classList.add('flex'); + + // Prevent background scroll, compensating for scrollbar width to avoid layout shift + const scrollbarWidth = + window.innerWidth - document.documentElement.clientWidth; + document.body.style.overflow = 'hidden'; + document.body.style.paddingRight = `${scrollbarWidth}px`; + + // Focus the modal for accessibility + calModal.focus(); +} + +export function closeEventModal(): void { + if (!calModal || !calOverlay) return; + calOverlay.classList.add('hidden'); + calOverlay.classList.remove('flex'); + calModal.classList.add('hidden'); + calModal.classList.remove('flex'); + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; +} From 52d3a576ea48da411933dd2b16ef2738a3eac80d Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Mon, 16 Feb 2026 17:45:37 -0800 Subject: [PATCH 14/14] Update AGENTS.md and linting/formatting --- .prettierrc.mjs | 8 ++ AGENTS.md | 153 ++++++++++++--------- package.json | 1 + pnpm-lock.yaml | 349 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 448 insertions(+), 63 deletions(-) diff --git a/.prettierrc.mjs b/.prettierrc.mjs index f084910..a56ce69 100644 --- a/.prettierrc.mjs +++ b/.prettierrc.mjs @@ -4,6 +4,14 @@ const config = { tabWidth: 4, semi: true, singleQuote: true, + overrides: [ + { + files: '*.md', + options: { + tabWidth: 2, + }, + }, + ], }; export default config; diff --git a/AGENTS.md b/AGENTS.md index 8d92d23..47f0f86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,8 +25,8 @@ This is the primary code validation tool. It runs TypeScript checking for both ` ### Linting -- **No ESLint** is configured. Do not add ESLint rules or dependencies. -- **Markdown linting** is handled in CI via `markdownlint-cli2`. Config in `.markdownlint.json` disables line length limits (MD013), inline HTML (MD033), and duplicate headings (MD024). +- **No ESLint** is configured. Do not add ESLint rules or dependencies. +- **Markdown linting** is handled in CI via `markdownlint-cli2`. Config in `.markdownlint.json` disables line length limits (MD013), inline HTML (MD033), and duplicate headings (MD024). ### Testing @@ -38,24 +38,26 @@ There is **no test framework** configured (no vitest, jest, playwright, etc.). T Configured in `.prettierrc.mjs`: -- **Indentation**: 4 spaces (also in `.editorconfig`) -- **Semicolons**: Always (`semi: true`) -- **Quotes**: Single quotes (`singleQuote: true`) -- **Trailing commas**: ES5 style (`trailingComma: 'es5'`) +- **Indentation**: 4 spaces (also in `.editorconfig`) +- **Semicolons**: Always (`semi: true`) +- **Quotes**: Single quotes (`singleQuote: true`) +- **Trailing commas**: ES5 style (`trailingComma: 'es5'`) ### TypeScript -- Extends `astro/tsconfigs/strict` with `strictNullChecks: true` -- No path aliases -- use relative imports (`../components/Foo.astro`) -- Use `import type` for type-only imports: - ```ts - import type { CollectionEntry } from 'astro:content'; - ``` -- **Avoid `as` type assertions.** They bypass the type checker and can hide bugs. Instead: - - Use type guards (`if ('key' in obj)`, `typeof x === 'string'`, etc.) - - Use `satisfies` when you want to validate a value matches a type without widening - - Narrow types with conditional checks rather than casting - - If a cast is truly unavoidable (e.g. working around a third-party library's incomplete types), add a comment explaining why +- Extends `astro/tsconfigs/strict` with `strictNullChecks: true` +- No path aliases -- use relative imports (`../components/Foo.astro`) +- Use `import type` for type-only imports: + + ```ts + import type { CollectionEntry } from 'astro:content'; + ``` + +- **Avoid `as` type assertions.** They bypass the type checker and can hide bugs. Instead: + - Use type guards (`if ('key' in obj)`, `typeof x === 'string'`, etc.) + - Use `satisfies` when you want to validate a value matches a type without widening + - Narrow types with conditional checks rather than casting + - If a cast is truly unavoidable (e.g. working around a third-party library's incomplete types), add a comment explaining why ### Imports @@ -113,12 +115,7 @@ const { title, description } = Astro.props; - - - - + @@ -126,47 +123,60 @@ const { title, description } = Astro.props; ### Component Size & Refactoring -Keep `.astro` components under **~300 lines**. When a component grows beyond that, refactor by extracting: +Keep `.astro` components under **~400 lines**. When a component grows beyond that, refactor by extracting: -- **Server-side logic** (data fetching, heavy computation) into `src/lib/*.ts` utility modules -- **Client-side helper functions** (pure functions, constants) into `src/scripts/*.ts` modules, imported by the component's `