From b192cd53bacfb5fe65700327c244682e882bfa24 Mon Sep 17 00:00:00 2001 From: KungRaseri Date: Fri, 5 Jun 2026 23:46:15 -0700 Subject: [PATCH] docs(copilot): add Copilot instructions for The Collective Hub - Add comprehensive project overview and core philosophy - Document file structure reference for the codebase - Create key files reference table for task-specific guidance - Include multi-tenant guidelines and site resolution flow --- .github/copilot-instructions.md | 154 +++++ .../instructions/admin-panel.instructions.md | 192 ++++++ .../api-route-patterns.instructions.md | 177 ++++++ .../auth-and-roles.instructions.md | 168 +++++ .github/instructions/bits-ui.instructions.md | 475 ++++++++++++++ .../cdn-and-assets.instructions.md | 198 ++++++ .../instructions/components.instructions.md | 123 ++++ .../database-schema.instructions.md | 338 ++++++++++ .../deployment-guide.instructions.md | 132 ++++ .../docs-workflow.instructions.md | 110 ++++ .github/instructions/icons.instructions.md | 186 ++++++ .../multi-tenant-architecture.instructions.md | 162 +++++ .../public-site-theming.instructions.md | 202 ++++++ .../instructions/server-ts.instructions.md | 176 ++++++ .../instructions/svelte-ts.instructions.md | 67 ++ .github/instructions/svelte5.instructions.md | 87 +++ .../instructions/tailwindcss.instructions.md | 114 ++++ .../testing-multi-tenant.instructions.md | 238 +++++++ .github/instructions/testing.instructions.md | 250 ++++++++ .github/prompts/generate-admin-page.prompt.md | 79 +++ .../prompts/generate-api-endpoint.prompt.md | 67 ++ .github/prompts/generate-api-test.prompt.md | 274 ++++++++ .../prompts/generate-component-test.prompt.md | 95 +++ .../prompts/generate-db-migration.prompt.md | 63 ++ .github/prompts/generate-seed-data.prompt.md | 90 +++ .../generate-svelte-component.prompt.md | 71 +++ .github/prompts/new-route.prompt.md | 80 +++ .github/prompts/test-coverage.prompt.md | 190 ++++++ .github/workflows/ci-web.yml | 132 ++++ .github/workflows/release.yml | 67 ++ AGENTS.md | 367 +++++++++++ package-lock.json | 122 +--- plans/identity-adaptation-plan.md | 584 ++++++++++++++++++ 33 files changed, 5710 insertions(+), 120 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/admin-panel.instructions.md create mode 100644 .github/instructions/api-route-patterns.instructions.md create mode 100644 .github/instructions/auth-and-roles.instructions.md create mode 100644 .github/instructions/bits-ui.instructions.md create mode 100644 .github/instructions/cdn-and-assets.instructions.md create mode 100644 .github/instructions/components.instructions.md create mode 100644 .github/instructions/database-schema.instructions.md create mode 100644 .github/instructions/deployment-guide.instructions.md create mode 100644 .github/instructions/docs-workflow.instructions.md create mode 100644 .github/instructions/icons.instructions.md create mode 100644 .github/instructions/multi-tenant-architecture.instructions.md create mode 100644 .github/instructions/public-site-theming.instructions.md create mode 100644 .github/instructions/server-ts.instructions.md create mode 100644 .github/instructions/svelte-ts.instructions.md create mode 100644 .github/instructions/svelte5.instructions.md create mode 100644 .github/instructions/tailwindcss.instructions.md create mode 100644 .github/instructions/testing-multi-tenant.instructions.md create mode 100644 .github/instructions/testing.instructions.md create mode 100644 .github/prompts/generate-admin-page.prompt.md create mode 100644 .github/prompts/generate-api-endpoint.prompt.md create mode 100644 .github/prompts/generate-api-test.prompt.md create mode 100644 .github/prompts/generate-component-test.prompt.md create mode 100644 .github/prompts/generate-db-migration.prompt.md create mode 100644 .github/prompts/generate-seed-data.prompt.md create mode 100644 .github/prompts/generate-svelte-component.prompt.md create mode 100644 .github/prompts/new-route.prompt.md create mode 100644 .github/prompts/test-coverage.prompt.md create mode 100644 .github/workflows/ci-web.yml create mode 100644 .github/workflows/release.yml create mode 100644 AGENTS.md create mode 100644 plans/identity-adaptation-plan.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..bc45bb3 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,154 @@ +# The Collective Hub — Copilot Instructions + +## Project Overview + +**The Collective Hub** is a reusable SvelteKit website template system for launching branded landing pages for online theater hosts, watch-party communities, and Discord communities. One codebase → multiple deployed sites → one shared database + CDN. + +**Core philosophy**: One codebase, multiple sites, no data leaks, maintainable by one person. + +--- + +## File Structure Reference + +``` +.github/ + instructions/ ← Development guidelines for The Collective Hub + multi-tenant-architecture.instructions.md + database-schema.instructions.md + auth-and-roles.instructions.md + deployment-guide.instructions.md + admin-panel.instructions.md + cdn-and-assets.instructions.md + public-site-theming.instructions.md + api-route-patterns.instructions.md + testing-multi-tenant.instructions.md + bits-ui.instructions.md + components.instructions.md + icons.instructions.md + server-ts.instructions.md + svelte-ts.instructions.md + svelte5.instructions.md + tailwindcss.instructions.md + testing.instructions.md + prompts/ ← Scaffolding templates for routes, components, API, DB + new-route.prompt.md + generate-component-test.prompt.md + generate-api-endpoint.prompt.md + generate-admin-page.prompt.md + generate-db-migration.prompt.md + generate-svelte-component.prompt.md + generate-seed-data.prompt.md + test-coverage.prompt.md + workflows/ ← CI/CD and release automation +docs/ ← Architecture, database, roadmap, environment docs +src/ ← SvelteKit application +``` + +--- + +## Key Files to Read When Working on Specific Tasks + +| When working on... | Read this first | +| ------------------ | --------------- | +| Understanding site resolution, data scoping, deployment model | [`multi-tenant-architecture.instructions.md`](.github/instructions/multi-tenant-architecture.instructions.md) | +| Database queries, schema changes, migrations | [`database-schema.instructions.md`](.github/instructions/database-schema.instructions.md) | +| Authentication, Discord OAuth, role-based access | [`auth-and-roles.instructions.md`](.github/instructions/auth-and-roles.instructions.md) | +| Deploying a new site, Docker, Coolify setup | [`deployment-guide.instructions.md`](.github/instructions/deployment-guide.instructions.md) | +| Admin panel pages, auth guards, form actions | [`admin-panel.instructions.md`](.github/instructions/admin-panel.instructions.md) | +| CDN storage, image upload, asset management | [`cdn-and-assets.instructions.md`](.github/instructions/cdn-and-assets.instructions.md) | +| Public landing page, theming, CSS custom properties | [`public-site-theming.instructions.md`](.github/instructions/public-site-theming.instructions.md) | +| API route conventions, validation, site scoping | [`api-route-patterns.instructions.md`](.github/instructions/api-route-patterns.instructions.md) | +| Writing tests with multi-tenant mocking patterns | [`testing-multi-tenant.instructions.md`](.github/instructions/testing-multi-tenant.instructions.md) | +| SvelteKit server-side code, Drizzle queries, form actions | [`server-ts.instructions.md`](.github/instructions/server-ts.instructions.md) | +| Svelte 5 runes, snippets, component patterns | [`svelte5.instructions.md`](.github/instructions/svelte5.instructions.md) | +| Tailwind CSS v4 styling | [`tailwindcss.instructions.md`](.github/instructions/tailwindcss.instructions.md) | +| Vitest + Playwright testing patterns | [`testing.instructions.md`](.github/instructions/testing.instructions.md) | + +--- + +## Multi-Tenant Guidelines + +### Site Resolution Flow + +``` +Request → hooks.server.ts (reads SITE_SLUG) → site-resolver.ts (queries DB by slug) + → locals.site / locals.siteSettings → app renders with site context +``` + +### Data Scoping Rule + +**Every table that owns site data must have a `siteId` column. Every query must filter by it.** + +```ts +// ✅ Always filter by siteId +const items = await db + .select() + .from(events) + .where(and(eq(events.siteId, locals.site.id), eq(events.isPublished, true))); +``` + +### CDN Key Rule + +**Store CDN keys (paths) in the database, never full URLs.** + +```ts +// Database stores: "sites/bad-movies-theater/logo.webp" +// Use cdnUrl() to get the full URL: +const url = cdnUrl(settings.branding.logoCdnKey); +``` + +### Auth Flow + +- `OWNER_DISCORD_ID` — bootstraps the site owner on first login +- `SUPER_ADMIN_DISCORD_IDS` — comma-separated, grants cross-site access +- Roles: `owner` > `admin` > `editor` + +--- + +## Tech Stack Instructions + +The web application is a **SvelteKit** project with TypeScript, PostgreSQL, Drizzle ORM, Better Auth, Tailwind CSS v4, Vitest, and Playwright. When generating or modifying code, read the relevant instruction file for detailed conventions. + +| File | Purpose | +| ---- | ------- | +| [`multi-tenant-architecture.instructions.md`](.github/instructions/multi-tenant-architecture.instructions.md) | Site resolution, data scoping, deployment model, env vars | +| [`database-schema.instructions.md`](.github/instructions/database-schema.instructions.md) | All tables, relationships, indexes, migration strategy | +| [`auth-and-roles.instructions.md`](.github/instructions/auth-and-roles.instructions.md) | Better Auth, Discord OAuth, role system, super admin | +| [`deployment-guide.instructions.md`](.github/instructions/deployment-guide.instructions.md) | Coolify multi-deploy, Docker, migration runner, env setup | +| [`admin-panel.instructions.md`](.github/instructions/admin-panel.instructions.md) | Admin layout, auth guards, form patterns, admin pages | +| [`cdn-and-assets.instructions.md`](.github/instructions/cdn-and-assets.instructions.md) | CDN helpers, image upload, webp conversion, URL construction | +| [`public-site-theming.instructions.md`](.github/instructions/public-site-theming.instructions.md) | SSR-only landing page, CSS custom properties, theme presets | +| [`api-route-patterns.instructions.md`](.github/instructions/api-route-patterns.instructions.md) | API route conventions, asset upload, event CRUD, validation | +| [`testing-multi-tenant.instructions.md`](.github/instructions/testing-multi-tenant.instructions.md) | Multi-tenant test patterns, mocking site context, auth testing | +| [`bits-ui.instructions.md`](.github/instructions/bits-ui.instructions.md) | Bits UI headless component patterns | +| [`components.instructions.md`](.github/instructions/components.instructions.md) | Svelte 5 component architecture | +| [`icons.instructions.md`](.github/instructions/icons.instructions.md) | Lucide icon usage guidelines | +| [`server-ts.instructions.md`](.github/instructions/server-ts.instructions.md) | SvelteKit server-side TypeScript patterns | +| [`svelte-ts.instructions.md`](.github/instructions/svelte-ts.instructions.md) | Svelte 5 .svelte.ts reactive module patterns | +| [`svelte5.instructions.md`](.github/instructions/svelte5.instructions.md) | Svelte 5 runes, snippets, migrations | +| [`tailwindcss.instructions.md`](.github/instructions/tailwindcss.instructions.md) | Tailwind CSS v4 configuration | +| [`testing.instructions.md`](.github/instructions/testing.instructions.md) | Vitest + Playwright base testing patterns | + +--- + +## Hard Rules + +1. **Always filter by `siteId`.** Every Drizzle query on site-owned data must include a `siteId` filter. Missing this is the most common multi-tenant bug and causes data leaks between sites. + +2. **Store CDN keys, not full URLs.** Database fields store paths like `sites/{slug}/logo.webp`. The `cdnUrl()` helper constructs full URLs using `CDN_BASE_URL`. Never store a full URL in the database. + +3. **One deployment runs migrations.** Only the deployment with `RUN_MIGRATIONS=true` runs database migrations. Deploy this one first when schema changes are included in a release. + +4. **No site-specific conditional logic.** Never write `if (site.slug === 'some-site')`. All per-site differences come from database settings. If a site genuinely needs a unique feature, build it as a configurable option for all sites. + +5. **Never commit `.env` files.** Environment variables are configured per-deployment in Coolify. The `.env` file is in `.gitignore` and must never be committed. + +6. **Use `$lib` aliases, not relative paths.** Always import from `$lib/server/db`, `$lib/components/`, etc. Never use relative imports like `../../lib/server/db`. + +7. **Co-locate tests.** Test files go next to the source they test. Server tests use `.test.ts`, browser component tests use `.svelte.test.ts`. + +8. **No `svelte-ignore` suppression comments.** All accessibility and type issues must be fixed properly. Never hide a warning behind a suppression comment. + +9. **Prefer additive migrations.** In production, add new columns and tables. Avoid destructive operations (ALTER DROP, RENAME). JSON settings columns reduce migration frequency. + +10. **Respect the phase roadmap.** Phase 1 must be fully working (public site + admin login + settings save) before Phase 2. A working simple site is more valuable than a half-built complex one. diff --git a/.github/instructions/admin-panel.instructions.md b/.github/instructions/admin-panel.instructions.md new file mode 100644 index 0000000..e26d701 --- /dev/null +++ b/.github/instructions/admin-panel.instructions.md @@ -0,0 +1,192 @@ +--- +description: 'Use when building or modifying admin panel pages for The Collective Hub. Covers admin route structure, auth guards, form action patterns, layout conventions, and admin Svelte component patterns.' +applyTo: 'src/routes/admin/**/*.svelte', 'src/routes/admin/**/*.server.ts' +--- + +# Admin Panel + +## Route Structure + +Admin routes live under `src/routes/admin/`: + +``` +src/routes/admin/ + +layout.server.ts ← Auth guard (redirects to /login if unauthenticated) + +layout.svelte ← Admin shell with navigation sidebar + +page.svelte ← Admin dashboard (overview) + settings/ + +page.server.ts ← Loads/saves site settings + +page.svelte + branding/ + +page.server.ts ← Logo, colors, theme + +page.svelte + homepage/ + +page.server.ts ← Hero text, about, CTA + +page.svelte + links/ + +page.server.ts ← Nav links + social links + +page.svelte + events/ + +page.server.ts ← Event CRUD + +page.svelte + assets/ + +page.server.ts ← Asset library + +page.svelte +``` + +## Auth Guard Pattern + +Every admin server load must check authentication. This is centralized in the admin layout: + +```ts +// src/routes/admin/+layout.server.ts +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + if (!locals.user) { + redirect(302, '/login'); + } + + // Optional: check minimum role for admin access + // const roleLevel = { owner: 3, admin: 2, editor: 1 }; + // if ((roleLevel[locals.membership?.role ?? ''] ?? 0) < 2) { + // redirect(302, '/'); + // } + + return { + user: locals.user, + membership: locals.membership, + settings: locals.siteSettings, + }; +}; +``` + +## Form Action Patterns + +Admin pages use SvelteKit form actions for data mutations. API routes (`+server.ts`) are reserved for asset uploads and external integrations. + +### Standard Form Action + +```ts +// +page.server.ts +import { fail, redirect } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { siteSettings } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) redirect(302, '/login'); + return { + settings: locals.siteSettings, + }; +}; + +export const actions: Actions = { + default: async ({ locals, request }) => { + if (!locals.user) return fail(401, { error: 'Unauthorized' }); + + const form = await request.formData(); + const siteName = form.get('siteName') as string; + + if (!siteName || siteName.trim().length === 0) { + return fail(400, { error: 'Site name is required' }); + } + + const updatedSettings = { + ...locals.siteSettings, + branding: { + ...locals.siteSettings?.branding, + siteName: siteName.trim(), + }, + }; + + await db + .update(siteSettings) + .set({ settings: updatedSettings }) + .where(eq(siteSettings.siteId, locals.site.id)); + + return { success: true, message: 'Settings saved' }; + }, +}; +``` + +### Form Component + +```svelte + + +
+
+ + +
+ + {#if form?.error} +

{form.error}

+ {/if} + {#if form?.success} +

{form.message}

+ {/if} + + +
+``` + +## Admin Layout Shell + +The admin layout provides consistent navigation: + +```svelte + + +
+ + + + +
+ {@render children()} +
+
+``` + +## Admin Page Conventions + +1. **Server load fetches current data** — always load from DB, don't rely on cached `locals` +2. **Form actions validate and save** — never trust client-side data without server validation +3. **Success/error feedback** — return `{ success: true, message: '...' }` or `fail(statusCode, { error: '...' })` +4. **Role-aware UI** — conditionally show/hide controls based on `membership.role` +5. **Optimistic updates** — not required for V1; simple form submit + reload is fine +6. **No client-side state management** — rely on `form` and `data` from SvelteKit's form actions diff --git a/.github/instructions/api-route-patterns.instructions.md b/.github/instructions/api-route-patterns.instructions.md new file mode 100644 index 0000000..5a504c9 --- /dev/null +++ b/.github/instructions/api-route-patterns.instructions.md @@ -0,0 +1,177 @@ +--- +description: 'Use when creating or modifying SvelteKit API endpoints (+server.ts) for The Collective Hub. Covers route conventions, authentication, validation, site scoping, and response formats.' +applyTo: 'src/routes/api/**/*.server.ts' +--- + +# API Route Patterns + +## Route Structure + +API routes live under `src/routes/api/`: + +``` +src/routes/api/ + auth/ + [...betterAuth]/ ← Better Auth handles this + assets/ + +server.ts ← Upload endpoint (POST) +``` + +Additional API endpoints should follow this pattern. + +## Conventions + +### File Naming + +- API handlers go in `+server.ts` files +- All HTTP methods are exported as named functions: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` +- Co-located test files drop the `+` prefix: `server.test.ts` + +### Response Format + +All API responses use SvelteKit's `json()` helper: + +```ts +import { json } from '@sveltejs/kit'; + +// Success +return json({ data: result }); + +// Created +return json({ data: newResource }, { status: 201 }); + +// Error +return json({ error: 'Resource not found' }, { status: 404 }); +``` + +## Authentication + +All API routes (except public ones) must check authentication: + +```ts +import { json } from '@sveltejs/kit'; + +export async function POST({ locals, request }) { + if (!locals.user) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Site scoping: always use locals.site.id + const siteId = locals.site.id; + + // ... handler logic +} +``` + +## Site Scoping + +Every API route that touches site-owned data must filter by `locals.site.id`: + +```ts +import { json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { events } from '$lib/server/db/schema'; +import { eq, and } from 'drizzle-orm'; + +export async function GET({ locals, url }) { + const siteId = locals.site.id; + + const items = await db + .select() + .from(events) + .where( + and( + eq(events.siteId, siteId), + eq(events.isPublished, true) + ) + ) + .orderBy(events.startTime); + + return json({ data: items }); +} +``` + +## Validation + +Validate request data on the server. Never trust client-provided data: + +```ts +import { json } from '@sveltejs/kit'; + +export async function POST({ locals, request }) { + if (!locals.user) return json({ error: 'Unauthorized' }, { status: 401 }); + + const body = await request.json(); + const { title, startTime } = body; + + // Manual validation + if (!title || typeof title !== 'string' || title.trim().length === 0) { + return json({ error: 'Title is required' }, { status: 400 }); + } + + if (!startTime || isNaN(Date.parse(startTime))) { + return json({ error: 'Valid start time is required' }, { status: 400 }); + } + + // ... proceed with valid data +} +``` + +For complex validation, consider using [Zod](https://zod.dev/) (add when validation complexity warrants it): + +```ts +import { z } from 'zod'; + +const eventSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().max(2000).optional(), + startTime: z.string().datetime(), + eventType: z.enum(['screening', 'watch_party', 'meetup', 'other']).optional(), +}); + +const result = eventSchema.safeParse(body); +if (!result.success) { + return json({ error: result.error.flatten() }, { status: 400 }); +} +``` + +## Asset Upload Endpoint + +The asset upload endpoint is the primary use case for API routes (most admin mutations use form actions instead). See [`cdn-and-assets.instructions.md`](cdn-and-assets.instructions.md) for the full upload pattern. + +## Form Actions vs API Routes + +| Concern | Use Form Actions | Use API Routes | +|---------|-----------------|----------------| +| Admin data mutations (settings, branding, content) | ✅ Form actions in `+page.server.ts` | ❌ | +| File uploads | ❌ | ✅ `+server.ts` (multipart) | +| External API proxies | ❌ | ✅ `+server.ts` | +| Third-party webhooks | ❌ | ✅ `+server.ts` | + +**Rule of thumb:** If the request comes from an admin page form, use form actions. If it comes from an external system or involves file upload, use API routes. + +## Error Handling + +```ts +import { json } from '@sveltejs/kit'; + +// 400 — Bad Request (validation) +return json({ error: 'Invalid input' }, { status: 400 }); + +// 401 — Unauthorized (not logged in) +return json({ error: 'Authentication required' }, { status: 401 }); + +// 403 — Forbidden (wrong role) +return json({ error: 'Insufficient permissions' }, { status: 403 }); + +// 404 — Not Found +return json({ error: 'Resource not found' }, { status: 404 }); + +// 500 — Server Error +try { + // risky operation +} catch (err) { + console.error('Failed to process request:', err); + return json({ error: 'Internal server error' }, { status: 500 }); +} +``` diff --git a/.github/instructions/auth-and-roles.instructions.md b/.github/instructions/auth-and-roles.instructions.md new file mode 100644 index 0000000..be60a7b --- /dev/null +++ b/.github/instructions/auth-and-roles.instructions.md @@ -0,0 +1,168 @@ +--- +description: 'Use when implementing or modifying authentication, authorization, role-based access, or login flows for The Collective Hub. Covers Better Auth with Discord OAuth, owner bootstrap, super admin access, and role hierarchy.' +applyTo: 'src/lib/server/auth.ts', 'src/hooks.server.ts', 'src/routes/**/*.server.ts', 'src/routes/**/*.svelte' +--- + +# Authentication & Roles + +## Auth Stack + +- **Library**: [Better Auth](https://www.better-auth.com/) v1.6+ +- **Provider**: Discord OAuth +- **Session**: Server-side sessions managed by Better Auth + +## Auth Flow + +```mermaid +sequenceDiagram + participant U as User + participant S as SvelteKit App + participant DA as Discord OAuth + participant DB as PostgreSQL + + U->>S: Visit /login + S->>DA: Redirect to Discord OAuth + DA->>U: Authorize + U->>S: Callback with OAuth code + S->>DA: Exchange code for token + user info + DA-->>S: Discord user (id, username, avatar) + S->>DB: Upsert user record + S->>DB: Check if Discord ID matches OWNER_DISCORD_ID + S->>DB: Check if Discord ID matches SUPER_ADMIN_DISCORD_IDS + DB-->>S: Match found → assign appropriate role + S->>S: Create session + S-->>U: Redirect to /admin (or show access denied) +``` + +## Better Auth Setup + +The auth instance is configured in [`src/lib/server/auth.ts`](src/lib/server/auth.ts): + +```ts +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from '$lib/server/db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: 'pg', + // Better Auth manages its own tables: user, session, account, verification + }), + socialProviders: { + discord: { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }, + }, +}); +``` + +SvelteKit integration in [`src/hooks.server.ts`](src/hooks.server.ts) handles the `/api/auth/*` routes via `svelteKitHandler`. + +## Owner Bootstrap + +On first login, the app compares the user's Discord ID against the `OWNER_DISCORD_ID` environment variable: + +1. A `membership` record is created (or confirmed) with role `owner` for the current site +2. The env var acts as a bootstrap — once the owner exists in the DB, the env var could be removed +3. Long-term, owners can add other admins/editors through the admin panel + +```ts +// Pseudocode for owner bootstrap logic +if (user.discordId === env.OWNER_DISCORD_ID) { + // Upsert membership with role 'owner' for current site + await db.insert(memberships).values({ + siteId: locals.site.id, + userId: user.id, + role: 'owner', + }).onConflictDoUpdate({ target: [memberships.siteId, memberships.userId], set: { role: 'owner' } }); +} +``` + +## Super Admin Access + +The app checks the user's Discord ID against `SUPER_ADMIN_DISCORD_IDS` (comma-separated): + +1. Matched users get cross-site admin access — bypass site-scoped membership checks +2. Super admins can access any site's admin panel regardless of `OWNER_DISCORD_ID` +3. Intended for David (system maintainer) to manage all sites +4. Checked on every request, not just at login + +```ts +const SUPER_ADMIN_IDS = (env.SUPER_ADMIN_DISCORD_IDS || '').split(',').map(id => id.trim()); +const isSuperAdmin = SUPER_ADMIN_IDS.includes(user.discordId); +``` + +## Role Hierarchy + +| Role | Permissions | Assigned By | +|------|-------------|-------------| +| `owner` | Full control. Can manage admins. One per site initially. | Bootstrap via `OWNER_DISCORD_ID` | +| `admin` | Can edit all site settings and content. Cannot delete site or manage owner. | Owner | +| `editor` | Can edit content (events, pages) but not site settings or branding. | Owner or Admin | + +### Checking Roles in Server Code + +```ts +import { redirect } from '@sveltejs/kit'; + +// Auth guard — redirect if not logged in +if (!locals.user) { + redirect(302, '/login'); +} + +// Role guard — check minimum role +const roleHierarchy = { owner: 3, admin: 2, editor: 1 }; +const userRole = locals.membership?.role; +const minRole = 'admin'; + +if (!userRole || (roleHierarchy[userRole] ?? 0) < (roleHierarchy[minRole] ?? 0)) { + // Check super admin bypass + if (!locals.isSuperAdmin) { + redirect(302, '/admin'); // or show 403 + } +} +``` + +### In Admin Layout Server Load + +```ts +// src/routes/admin/+layout.server.ts +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + if (!locals.user) { + redirect(302, '/login'); + } + + return { + user: locals.user, + membership: locals.membership, + isSuperAdmin: locals.isSuperAdmin, + }; +}; +``` + +### In Page Components + +```svelte + + +{#if canEdit} + +{/if} +``` + +## Session Handling + +Better Auth manages sessions automatically. Key patterns: + +- **Check login status**: `locals.user` is set by the auth handler in the hooks +- **Logout**: POST to `/api/auth/signout` +- **Session persistence**: Better Auth uses cookies; no client-side token storage needed +- **No client-side auth state**: Auth state comes from server load functions, never from `$page.data` derived values on the client (use server load functions exclusively) diff --git a/.github/instructions/bits-ui.instructions.md b/.github/instructions/bits-ui.instructions.md new file mode 100644 index 0000000..6f0568c --- /dev/null +++ b/.github/instructions/bits-ui.instructions.md @@ -0,0 +1,475 @@ +--- +description: 'Use when integrating Bits UI headless components into The Collective Hub. Covers installation, the child snippet pattern, component catalog, Tailwind v4 integration, and migration guidance for overlapping existing UI components.' +applyTo: 'src/**/*.svelte' +--- + +# Bits UI Integration + +## Overview + +[Bits UI](https://bits-ui.com/) is a **headless component library for Svelte 5**, built and maintained by Hunter Johnston ([@huntabyte](https://github.com/huntabyte)). It is the successor to Melt UI, rebuilt exclusively for Svelte 5 runes. + +| Property | Description | +| ----------------------- | ---------------------------------------------------------------------------------------- | +| **Headless** | Ships unstyled — you bring your own CSS/Tailwind classes | +| **Accessibility-first** | WAI-ARIA compliant with keyboard navigation, focus management, and screen reader support | +| **TypeScript-native** | Full type coverage with exported type helpers | +| **Svelte 5 only** | Built exclusively for runes (`$state`, `$derived`, `$props`, `$bindable`, `Snippet`) | + +## Installation + +```bash +npm install bits-ui +``` + +For date/time components, also install the peer dependency: + +```bash +npm install @internationalized/date +``` + +No additional setup or configuration required — import directly: + +```svelte + +``` + +## Component Catalog + +### Interactive / Disclosure + +| Component | Description | +| ---------------- | --------------------------------------- | +| `Accordion` | Expandable/collapsible content sections | +| `AlertDialog` | Modal dialog for urgent confirmations | +| `Collapsible` | Single collapsible panel | +| `Dialog` | Modal dialog overlay | +| `Tabs` | Tabbed content switcher | +| `DropdownMenu` | Menu anchored to a trigger button | +| `ContextMenu` | Right-click context menu | +| `Menubar` | Horizontal menu bar with nested menus | +| `NavigationMenu` | Responsive nav menu with submenus | +| `Popover` | Floating content anchored to a trigger | +| `Tooltip` | Hover tooltip anchored to an element | +| `LinkPreview` | Floating preview card for hyperlinks | +| `Command` | ⌘K-style command palette | +| `Combobox` | Autocomplete input with dropdown | +| `Select` | Single/multi-select dropdown | + +### Form / Input + +| Component | Description | +| ----------------- | -------------------------------- | +| `Button` | Unstyled button primitive | +| `Checkbox` | Checkbox input | +| `Label` | Accessible label | +| `RadioGroup` | Radio button group | +| `Switch` | Toggle switch | +| `Toggle` | Single toggle button | +| `ToggleGroup` | Grouped toggle buttons | +| `PinInput` | Split-digit code input | +| `Slider` | Range slider | +| `Meter` | Meter/guage display | +| `Progress` | Progress bar | +| `RatingGroup` | Star/score rating input | +| `Calendar` | Single date calendar | +| `RangeCalendar` | Date range calendar | +| `DateField` | Single date segmented input | +| `DateRangeField` | Date range segmented input | +| `DatePicker` | Calendar popover for single date | +| `DateRangePicker` | Calendar popover for date range | +| `TimeField` | Time segmented input | +| `TimeRangeField` | Time range segmented input | + +### Layout / Display + +| Component | Description | +| ------------- | ------------------------------- | +| `AspectRatio` | Fixed aspect ratio container | +| `Avatar` | Image placeholder with fallback | +| `Badge` | Inline status/label indicator | +| `Separator` | Horizontal/vertical divider | +| `Pagination` | Page navigation control | +| `ScrollArea` | Custom scrollbar container | +| `Toolbar` | Toolbar button group | + +### Utilities & Type Helpers + +| Export | Description | +| ------------------------ | ---------------------------------------------------------- | +| `mergeProps` | Merge multiple prop objects with handler chaining | +| `useId` | Generate unique IDs for accessibility wiring | +| `Portal` | Teleport content to a different DOM node | +| `isUsingKeyboard` | Track keyboard vs. mouse interaction | +| `BitsConfig` | Global Bits UI configuration provider | +| `computeCommandScore` | Score items for Command/Combobox filtering | +| `WithElementRef` | Type helper for element ref forwarding | +| `WithoutChild` | Type helper omitting the child snippet prop | +| `WithoutChildren` | Type helper omitting children snippet props | +| `WithoutChildrenOrChild` | Type helper omitting both child and children snippet props | + +## Svelte 5 Runes Integration + +Bits UI is built exclusively for Svelte 5 runes. It uses a **compound component pattern** (`Component.Root`, `Component.Item`, `Component.Trigger`, `Component.Content`, etc.) throughout. + +### Bindable Props + +Control component state with two-way bindable props: + +```svelte + + + + + +``` + +Common bindable props across components: + +| Pattern | Example | +| ------------------ | ------------------------------------------ | +| `bind:open` | Dialog, Popover, DropdownMenu, Collapsible | +| `bind:value` | Select, Combobox, Tabs, RadioGroup | +| `bind:placeholder` | DateField, DatePicker | +| `bind:date` | Calendar, DatePicker | + +### Callback Props + +Bits UI uses an `on{EventName}Change` pattern for change callbacks: + +```svelte + console.log('selected', val)} +> +``` + +## The `child` Snippet Pattern (Critical) + +Bits UI does **not** use ``. Instead, it exposes a **`child` snippet** pattern for render delegation. + +### Basic Usage + +```svelte + + {#snippet child({ props })} + + {/snippet} + +``` + +The `child` snippet receives an object with `props` (and optionally `wrapperProps` for floating components). You must spread `{...props}` onto the root element of your rendered markup to wire up accessibility, events, and state attributes. + +### Floating Components (Two-Level Wrapper) + +For **floating components** — `Popover`, `Tooltip`, `DropdownMenu`, `Select`, `Combobox`, `DatePicker`, `Command` — Bits UI requires a **two-level wrapper** inside the `child` snippet: + +```svelte + + + + + {#snippet child({ props })} + + {/snippet} + + + + {#snippet child({ wrapperProps, props })} +
+
+ +
+
+ {/snippet} +
+
+``` + +The snippet receives: + +- `wrapperProps` — spread onto the outermost positioning wrapper +- `props` — spread onto the actual UI element +- `open` — current open state (available but not needed for conditional rendering) + +⚠️ **Do NOT wrap floating content in `{#if open}`** — bits-ui's `PresenceManager` handles visibility internally. Premature DOM removal via `{#if open}` breaks the animation lifecycle and causes `ScrollLock` to persist indefinitely (`document.body.style.pointerEvents = "none"` never gets cleared). See [Anti-Patterns](#anti-patterns--pitfalls). + +## Tailwind CSS v4 Integration + +Bits UI works naturally with the project's Tailwind v4 setup (see [`tailwindcss.instructions.md`](.github/instructions/tailwindcss.instructions.md)). + +### Passing Classes + +Pass `class` directly to any Bits UI component — they forward it to the root DOM element: + +```svelte + +``` + +### Data-Attribute Selectors + +Bits UI components expose `data-*` attributes on their root elements for state-driven styling: + +| Attribute | Applies When | +| ------------------------- | ---------------------------------- | +| `[data-state="open"]` | Component is open/expanded | +| `[data-state="closed"]` | Component is closed/collapsed | +| `[data-state="active"]` | Tab or item is active | +| `[data-state="inactive"]` | Tab or item is inactive | +| `[data-disabled]` | Component or item is disabled | +| `[data-highlighted]` | Item is highlighted (keyboard nav) | +| `[data-selected]` | Item is selected | + +Example usage in a CSS block: + +```css +[data-state='open'] { + --tw-ring-color: var(--color-cyan-500); +} +``` + +### CSS Variables for Layout + +Bits UI exposes CSS variables for measuring dynamic content: + +| Variable | Component | +| --------------------------------- | ------------------- | +| `--bits-accordion-content-height` | `Accordion.Content` | +| `--bits-select-anchor-width` | `Select.Content` | +| `--bits-tooltip-trigger-width` | `Tooltip` | +| `--bits-popover-anchor-width` | `Popover.Content` | + +### Animations + +Use `data-starting-style` and `data-ending-style` transient attributes for CSS-based enter/exit animations. These attributes are present only during the animation frame and are automatically removed. + +## Project-Specific Integration + +This project's technology choices align well with Bits UI: + +| Project Trait | Bits UI Compatibility | +| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **Tailwind CSS v4** (CSS-first config) | `class` prop pattern works directly — no `tailwind.config.js` required | +| **Svelte 5 runes only** (see [`svelte5.instructions.md`](.github/instructions/svelte5.instructions.md)) | Natively compatible — Bits UI is built on runes | +| **No `tailwind.config.js`** | Bits UI does not require one | +| **Colors follow brand palette** | Use existing design tokens (see [`tailwindcss.instructions.md`](.github/instructions/tailwindcss.instructions.md)) | +| **Lucide icons** (see [`icons.instructions.md`](.github/instructions/icons.instructions.md)) | Pass icon components as snippets to Bits UI components | + +### Icon Pattern with Bits UI + +Render Lucide SVG strings inside child snippets: + +```svelte + + {#snippet child({ props })} + + {/snippet} + +``` + +### `{@html}` for Inline SVGs — Last-Resort Pattern + +The use of `{@html}` to render inline SVG strings is a **last-resort pattern** in this project. Follow the icon hierarchy below: + +1. **`@lucide/svelte` components** — preferred for all UI icons (e.g., ``, ``) +2. **`simple-icons`** — for brand icons (social media platforms, tech logos) +3. **Inline SVG via `{@html}`** — only for Bits UI child snippets where component pass-through isn't possible + +> See [`icons.instructions.md`](.github/instructions/icons.instructions.md) for the full icon hierarchy and usage guidelines. + +## Existing Component Overlap & Migration Guidance + +The following custom components may have overlapping functionality with Bits UI. Migration should be **phased and deliberate** — never replace a tested, working component without a clear benefit. + +| Component Type | Bits UI Equivalent | Priority | Rationale | +| ------------------- | ------------------------- | -------- | ------------------------------------------------------------------ | +| Custom modal/dialog | `Dialog` | Medium | Accessibility upgrade (focus trapping, ARIA, keyboard dismiss) | +| Tabs/accordion | `Tabs` / `Accordion` | Low | Existing implementation works; reconsider if a11y issues arise | +| Custom select | `Select` | Medium | Accessibility upgrade (keyboard nav, ARIA combobox pattern) | +| Custom button | `Button` | Low | Keep project-specific variants | +| (none) | `Popover` | **High** | New capability — useful for dropdown menus, filters, quick-actions | +| (none) | `Tooltip` | **High** | New capability — useful for icon-only buttons and truncated text | +| (none) | `Calendar` / `DatePicker` | Medium | New capability — date picking for scheduling | +| (none) | `DropdownMenu` | **High** | New capability — contextual menus | + +### Migration Rules + +1. **Never migrate a tested, working component** solely for library adoption — there must be a measurable benefit (accessibility, maintainability, feature gap) +2. When migrating, **keep the old component in place** during a transition period — do not break existing consumers +3. Wrap Bits UI components in project-specific wrappers if the same styling is used in many places (e.g., `AppPopover.svelte` that pre-applies project styling) +4. Run existing tests after any migration to confirm nothing is broken + +## Usage Examples with Project Styling + +### Dialog — Confirmation Modal + +```svelte + + + + + + + {#snippet child({ wrapperProps, props })} +
+
+ + Confirm Action + + Are you sure you want to proceed? This action cannot be undone. + + +
+ + +
+
+
+ {/snippet} +
+
+``` + +### Popover — Filter Dropdown + +```svelte + + + + + {#snippet child({ props })} + + {/snippet} + + + + {#snippet child({ wrapperProps, props })} +
+
+

+ Filter by Status +

+ {#each ['All', 'Published', 'Draft', 'Archived'] as option} + + {/each} +
+
+ {/snippet} +
+
+``` + +### Accordion — FAQ / Settings Sections + +```svelte + + + + + + {#snippet child({ props, open })} + + {/snippet} + + + {#snippet child({ props })} +
+ Content for section one. +
+ {/snippet} +
+
+ + + + {#snippet child({ props, open })} + + {/snippet} + + + {#snippet child({ props })} +
+ Content for section two. +
+ {/snippet} +
+
+
+``` + +## Anti-Patterns / Pitfalls + +- ❌ **Don't use Bits UI with Svelte 4** — it requires Svelte 5 runes and will not work +- ❌ **Don't use `` inside Bits UI components** — always use `{#snippet child(...)}` instead (see [`svelte5.instructions.md`](.github/instructions/svelte5.instructions.md)) +- ❌ **Don't forget the two-level wrapper for floating components** — `Popover`, `Tooltip`, `DropdownMenu`, `Select`, `Combobox`, `DatePicker`, and `Command` content snippets receive both `wrapperProps` and `props` +- ❌ **Don't wrap floating content in `{#if open}`** — bits-ui's `PresenceManager` handles visibility internally. Premature DOM removal via `{#if open}` prevents `AnimationsComplete.run()` from firing its completion callback, which causes `ScrollLock` to persist indefinitely (`document.body.style.pointerEvents = "none"` and `document.body.style.overflow = "hidden"` never get cleared). This was a critical bug discovered and fixed across all floating components during Batch 5. +- ❌ **Don't override internal handlers without `mergeProps`** — if you need to add your own event handlers while keeping Bits UI's internal handlers, import `mergeProps` from `"bits-ui"` +- ❌ **Don't import from deep subpaths** — always import from `"bits-ui"` (e.g., `import { Dialog } from "bits-ui"`, not `import Dialog from "bits-ui/dialog"`) + +## Resources + +| Resource | Link | +| ----------------------------- | -------------------------------------------------------------------- | +| Full Documentation (LLMs.txt) | [bits-ui.com/docs/llms.txt](https://bits-ui.com/docs/llms.txt) | +| GitHub Repository | [github.com/huntabyte/bits-ui](https://github.com/huntabyte/bits-ui) | +| Author | Hunter Johnston ([@huntabyte](https://github.com/huntabyte)) | diff --git a/.github/instructions/cdn-and-assets.instructions.md b/.github/instructions/cdn-and-assets.instructions.md new file mode 100644 index 0000000..1349e17 --- /dev/null +++ b/.github/instructions/cdn-and-assets.instructions.md @@ -0,0 +1,198 @@ +--- +description: 'Use when implementing or modifying CDN storage, image upload, asset management, or URL construction for The Collective Hub. Covers the CDN helper, upload flow, image processing with sharp, and asset library patterns.' +applyTo: 'src/lib/server/cdn.ts', 'src/routes/api/assets/*.ts', 'src/routes/admin/assets/*.svelte' +--- + +# CDN & Assets + +## Architecture + +The Collective Hub stores **only CDN keys (paths) in the database**. Full URLs are constructed at render time using the `CDN_BASE_URL` environment variable. + +``` +Database stores: "sites/bad-movies-theater/logo.webp" +CDN_BASE_URL: "https://cdn.example.com" +Full URL: "https://cdn.example.com/sites/bad-movies-theater/logo.webp" +``` + +This means: +- **No URL updates needed** if the CDN provider changes — just change `CDN_BASE_URL` +- **No full URLs in the database** — ever +- **CDN migration is trivial** — one env var change + +## CDN Path Convention + +``` +sites/{siteSlug}/{type}/{filename} +``` + +Examples: +``` +sites/bad-movies-theater/logo.webp +sites/bad-movies-theater/background.webp +sites/bad-movies-theater/events/movie-night-june.webp +sites/garbage-day/logo.webp +``` + +## CDN Helper + +See [`src/lib/server/cdn.ts`](src/lib/server/cdn.ts): + +```ts +import { env } from '$env/dynamic/private'; + +const BASE_URL = env.CDN_BASE_URL; + +/** + * Construct a full CDN URL from a key. + * Returns null if the key is null or empty. + */ +export function cdnUrl(key: string | null): string | null { + if (!key) return null; + // Ensure no double slashes + const cleanKey = key.startsWith('/') ? key.slice(1) : key; + return `${BASE_URL}/${cleanKey}`; +} + +/** + * Construct a CDN key for a given site, type, and filename. + */ +export function cdnKey(siteSlug: string, type: string, filename: string): string { + return `sites/${siteSlug}/${type}/${filename}`; +} +``` + +## Upload Flow + +``` +User selects file + → Client-side validation (type, size) + → POST to /api/assets/upload (multipart form) + → Server-side validation + → sharp converts to webp + optimizes + → Upload to CDN storage + → Create asset record in database + → Return asset URL +``` + +### Server-Side Upload Handler + +```ts +// src/routes/api/assets/+server.ts +import { json } from '@sveltejs/kit'; +import sharp from 'sharp'; +import { env } from '$env/dynamic/private'; +import { db } from '$lib/server/db'; +import { assets } from '$lib/server/db/schema'; +import { cdnKey } from '$lib/server/cdn'; + +export async function POST({ locals, request }) { + if (!locals.user) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + const form = await request.formData(); + const file = form.get('file') as File; + + if (!file) { + return json({ error: 'No file provided' }, { status: 400 }); + } + + // Validate file type + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + return json({ error: 'Invalid file type' }, { status: 400 }); + } + + // Validate file size (max 10MB) + if (file.size > 10 * 1024 * 1024) { + return json({ error: 'File too large' }, { status: 400 }); + } + + // Convert to webp and optimize + const buffer = Buffer.from(await file.arrayBuffer()); + const webpBuffer = await sharp(buffer) + .webp({ quality: 80, lossless: false }) + .toBuffer(); + + // Generate CDN key + const filename = `${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '_')}.webp`; + const key = cdnKey(locals.siteSlug, 'uploads', filename); + + // Upload to CDN (implementation depends on CDN provider) + // await uploadToCdn(key, webpBuffer, 'image/webp'); + + // Create asset record + const [asset] = await db.insert(assets).values({ + siteId: locals.site.id, + uploadedByUserId: locals.user.id, + type: 'image', + filename: file.name, + mimeType: 'image/webp', + size: webpBuffer.length, + cdnKey: key, + altText: (form.get('altText') as string) || '', + }).returning(); + + return json({ asset, url: cdnUrl(key) }); +} +``` + +## Image Processing with Sharp + +All uploaded images are converted to **webp** format: + +```ts +import sharp from 'sharp'; + +// Convert to webp with quality optimization +const webpBuffer = await sharp(inputBuffer) + .webp({ quality: 80, lossless: false }) + .toBuffer(); + +// Resize if needed (e.g., for hero backgrounds) +const resized = await sharp(inputBuffer) + .resize(1920, 1080, { fit: 'cover', position: 'centre' }) + .webp({ quality: 80 }) + .toBuffer(); +``` + +## Asset Library (Admin) + +The assets page in the admin panel allows site owners to: +1. **Browse** all uploaded assets for their site +2. **Upload** new files (drag-and-drop or file picker) +3. **Copy CDN URLs** for use in branding/content settings +4. **Delete** assets (with confirmation) +5. **Edit alt text** for accessibility + +```svelte + + + +
+ {asset.altText} +

{asset.filename}

+ +
+``` + +## Key Rules + +1. **Never store full CDN URLs** in the database — only keys +2. **Never hardcode `CDN_BASE_URL`** in code — always use `env.CDN_BASE_URL` +3. **Always validate file types and sizes** on the server — never trust client validation alone +4. **Always convert to webp** for consistent format and smaller files +5. **Always scope assets by `siteId`** in queries +6. **CDN keys must not start with `/`** — the helper handles this, but be consistent diff --git a/.github/instructions/components.instructions.md b/.github/instructions/components.instructions.md new file mode 100644 index 0000000..5e8f472 --- /dev/null +++ b/.github/instructions/components.instructions.md @@ -0,0 +1,123 @@ +--- +description: 'Use when creating, editing, or organizing Svelte components for The Collective Hub. Enforces folder structure, naming, prop typing, and composition conventions for src/lib/components/.' +applyTo: 'src/lib/components/**/*.svelte' +--- + +# Component Conventions + +## Folder Structure + +``` +src/lib/components/ + ui/ ← Generic, reusable UI primitives (no business logic) + layout/ ← Structural shell components (Header, Footer, Navigation) +``` + +Place new components in the most specific folder: + +- If it's a general-purpose display or form element → `ui/` +- If it's a page-level structural shell → `layout/` +- Do **not** create new folders without a clear category need + +## Naming + +- Files: **PascalCase** — `SectionCard.svelte`, `StatusBadge.svelte` +- One component per file +- Name reflects what it **is**, not what it **does**: `StatCard` not `DisplayStat` + +## Imports + +Always import from the full `$lib/components/...` path — do not use relative paths from within `src/`: + +```ts +// ✅ +import SectionCard from '$lib/components/ui/SectionCard.svelte'; + +// ❌ +import SectionCard from '../components/ui/SectionCard.svelte'; +``` + +## Props + +Type props inline using `$props<{...}>()` — do not use a separate `interface` or `type` alias unless it's shared across multiple components: + +```svelte + +``` + +- Optional props must have a default value (`= undefined` or a real default) +- Use `Snippet` from `'svelte'` for composable markup slots +- Use `Snippet<[T]>` for typed render props (e.g., row renderers in `DataTable`) + +## Composition: Snippets over Slots + +Accept content via `Snippet` props. Common pattern: + +```svelte + +{#if headerAction} + {@render headerAction()} +{/if} +{@render children()} +``` + +Named snippets in the consumer: + +```svelte +{#snippet headerAction()} + +{/snippet} + + + + +``` + +> **Note on dynamic components:** In Svelte 5, `` is deprecated. Use `{@const ComponentVar = ...}` in the template and render it as `` instead. See [`svelte5.instructions.md`](svelte5.instructions.md#deprecated-apis) for details. + +### No `svelte-ignore` Suppression + +`svelte-ignore` comments must NOT be used to suppress svelte-check errors or warnings. All accessibility and type issues must be fixed properly — never hidden behind a suppression comment. + +## Admin Components + +Admin-specific components live alongside their route pages in `src/routes/admin/` rather than in `src/lib/components/`. Shared admin UI patterns (form fields, buttons, layouts) should be placed in `src/lib/components/admin/` if reused across multiple admin pages. + +## Current Component Inventory + +| Component | Path | Description | +|-----------|------|-------------| +| Public site sections | [route-level](src/routes/) | Hero, About, Events, Social Links — defined in the route or as page-specific components | +| Layout components | [`$lib/components/layout/`](src/lib/components/layout/) | Header, Footer, Navigation | +| UI primitives | [`$lib/components/ui/`](src/lib/components/ui/) | Container, SectionHeading, buttons, cards | + +> The component inventory evolves as the platform grows. When adding a new component, place it in the most specific folder: `ui/` for generic UI, `layout/` for structural shells, or a route directory for page-specific components. + +## Visual Style + +All components follow The Collective Hub theme system — see [`public-site-theming.instructions.md`](public-site-theming.instructions.md) for CSS custom property conventions and [`tailwindcss.instructions.md`](tailwindcss.instructions.md) for token and color usage. + +Use CSS custom properties for theme-aware colors: +```svelte +
+

Section Title

+ ... +
+``` + +- `rounded-lg` — standard border radius +- Use CSS custom properties for all colors (never hardcode) +- Use consistent spacing and alignment diff --git a/.github/instructions/database-schema.instructions.md b/.github/instructions/database-schema.instructions.md new file mode 100644 index 0000000..22e47fa --- /dev/null +++ b/.github/instructions/database-schema.instructions.md @@ -0,0 +1,338 @@ +--- +description: 'Use when creating or modifying database tables, writing Drizzle queries, adding migrations, or understanding the schema. Covers all tables, relationships, indexes, migration strategy, and Drizzle ORM patterns for The Collective Hub.' +applyTo: 'src/lib/server/db/**/*.ts', 'drizzle/**/*.sql' +--- + +# Database Schema + +## Design Principles + +- **`siteId` on every site-owned table** — non-negotiable +- **Prefer normalized tables over JSON columns** — except for theme/branding settings where flexibility is valuable +- **Timestamps on every table** (`createdAt`, `updatedAt`) +- **Use UUIDs for primary keys** — avoids sequential ID enumeration and works well distributed +- **Index `siteId` on every table that has it** — it's the most common query filter +- **Soft deletes where appropriate** — prefer `deletedAt` over hard deletes + +## Entity Relationship + +```mermaid +erDiagram + sites ||--o{ memberships : "has" + sites ||--|| siteSettings : "has" + sites ||--o{ assets : "owns" + sites ||--o{ navLinks : "has" + sites ||--o{ socialLinks : "has" + sites ||--o{ events : "hosts" + users ||--o{ memberships : "has" + users ||--o{ assets : "uploads" + + sites { + uuid id PK + text slug UK + text name + boolean isActive + } + + users { + uuid id PK + text discordId UK + text discordUsername + text discordAvatar + } + + memberships { + uuid id PK + uuid siteId FK + uuid userId FK + enum role + } + + siteSettings { + uuid id PK + uuid siteId FK UK + jsonb settings + } + + events { + uuid id PK + uuid siteId FK + text title + timestamptz startTime + boolean isPublished + } +``` + +## Table Reference + +### `sites` + +The core tenant table. One row per deployed site. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | `defaultRandom()` | +| `slug` | `text` (UNIQUE, NOT NULL) | Matches `SITE_SLUG` env var | +| `name` | `text` (NOT NULL) | Display name | +| `isActive` | `boolean` (default true) | Soft disable a site | +| `createdAt` | `timestamptz` | `defaultNow()` | +| `updatedAt` | `timestamptz` | `defaultNow()` + `$onUpdate()` | + +**Indexes:** UNIQUE on `slug`. + +### `users` + +Auth users. Created automatically on first Discord login. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | `defaultRandom()` | +| `discordId` | `text` (UNIQUE, NOT NULL) | Discord user ID | +| `discordUsername` | `text` | Display name from Discord | +| `discordAvatar` | `text` | Avatar hash/URL from Discord | +| `email` | `text` | If available from Discord scope | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | +| `lastLoginAt` | `timestamptz` | | + +**Indexes:** UNIQUE on `discordId`. + +> Note: Better Auth manages its own session/account tables. The `users` table here is the application-level user profile. + +### `memberships` + +Links users to sites with a role. A user can be a member of multiple sites with different roles. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (NOT NULL) | | +| `userId` | `uuid` → `users.id` (NOT NULL) | | +| `role` | `enum('owner', 'admin', 'editor')` | See role definitions | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** UNIQUE on `(siteId, userId)`. INDEX on `siteId`. INDEX on `userId`. + +### `siteSettings` + +Single JSON column per site for all configuration. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (UNIQUE, NOT NULL) | One settings row per site | +| `settings` | `jsonb` (NOT NULL, default `{}`) | All site settings as JSON | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** UNIQUE on `siteId`. + +The `settings` JSON structure (typed in [`src/lib/shared/types.ts`](src/lib/shared/types.ts)): + +```typescript +interface SiteSettingsData { + branding: { + siteName: string; + tagline: string; + logoCdnKey: string | null; + backgroundCdnKey: string | null; + faviconCdnKey: string | null; + }; + theme: { + preset: 'dark' | 'light' | 'custom'; + accentColor: string; + backgroundColor: string; + textColor: string; + }; + homepage: { + heroTitle: string; + heroSubtitle: string; + aboutText: string; + primaryButtonText: string; + primaryButtonLink: string; + showNextEvent: boolean; + showSchedule: boolean; + }; + layout: { + preset: 'standard'; + }; +} +``` + +**Why JSON for settings?** Settings are read as a batch, rarely queried individually, and benefit from schema flexibility. Adding a new setting requires no migration. + +### `assets` + +Records of uploaded media files stored in the CDN. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (NOT NULL) | | +| `uploadedByUserId` | `uuid` → `users.id` | Nullable for system assets | +| `type` | `text` (NOT NULL) | e.g., `image`, `document` | +| `filename` | `text` (NOT NULL) | Original filename | +| `mimeType` | `text` | e.g., `image/webp` | +| `size` | `integer` | Bytes | +| `cdnKey` | `text` (NOT NULL) | Path within CDN bucket | +| `altText` | `text` | Accessibility description | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** INDEX on `siteId`. INDEX on `cdnKey`. + +### `navLinks` + +Custom navigation links for a site's header/footer. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (NOT NULL) | | +| `label` | `text` (NOT NULL) | Display text | +| `url` | `text` (NOT NULL) | Link target | +| `position` | `text` (default `'header'`) | `header` or `footer` | +| `sortOrder` | `integer` (default 0) | Ordering within position | +| `isExternal` | `boolean` (default true) | Open in new tab? | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** INDEX on `(siteId, position, sortOrder)`. + +### `socialLinks` + +Social media / external platform links. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (NOT NULL) | | +| `platform` | `text` (NOT NULL) | e.g., `discord`, `twitter`, `youtube`, `twitch` | +| `label` | `text` | Display label, defaults to platform name | +| `url` | `text` (NOT NULL) | | +| `icon` | `text` | Icon identifier if custom | +| `sortOrder` | `integer` (default 0) | | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** INDEX on `(siteId, sortOrder)`. + +### `events` + +Scheduled events / watch parties / screenings. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `uuid` (PK) | | +| `siteId` | `uuid` → `sites.id` (NOT NULL) | | +| `title` | `text` (NOT NULL) | | +| `description` | `text` | | +| `eventType` | `text` (default `'screening'`) | `screening`, `watch_party`, `meetup`, `other` | +| `startTime` | `timestamptz` (NOT NULL) | | +| `endTime` | `timestamptz` | Optional duration | +| `timezone` | `text` (default `'America/New_York'`) | IANA timezone | +| `location` | `text` | e.g., "Discord Stage", "VR Chat" | +| `externalLink` | `text` | Link to event page, stream | +| `imageCdnKey` | `text` | Optional event image | +| `isPublished` | `boolean` (default false) | Draft mode | +| `isRecurring` | `boolean` (default false) | Placeholder for future | +| `createdAt` | `timestamptz` | | +| `updatedAt` | `timestamptz` | | + +**Indexes:** INDEX on `(siteId, startTime)`. INDEX on `(siteId, isPublished)`. + +## Drizzle Patterns + +### Schema Definition ([`src/lib/server/db/schema.ts`](src/lib/server/db/schema.ts)) + +```ts +import { pgTable, uuid, text, boolean, timestamp, integer, jsonb, index, uniqueIndex, pgEnum } from 'drizzle-orm/pg-core'; + +export const roleEnum = pgEnum('role', ['owner', 'admin', 'editor']); + +export const events = pgTable( + 'events', + { + id: uuid('id').defaultRandom().primaryKey(), + siteId: uuid('site_id').notNull().references(() => sites.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + // ... + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()), + }, + (table) => [ + index('events_site_id_start_time_idx').on(table.siteId, table.startTime), + ] +); +``` + +### Querying with Site Scope + +```ts +import { eq, and, desc, asc } from 'drizzle-orm'; +import { db } from '$lib/server/db'; +import { events } from '$lib/server/db/schema'; + +// Scoped to current site +const upcomingEvents = await db + .select() + .from(events) + .where( + and( + eq(events.siteId, locals.site.id), + eq(events.isPublished, true), + gte(events.startTime, new Date()) + ) + ) + .orderBy(asc(events.startTime)); +``` + +### Inserting + +```ts +const [newEvent] = await db + .insert(events) + .values({ + siteId: locals.site.id, + title: 'Bad Movie Night', + startTime: new Date('2025-06-15T20:00:00Z'), + isPublished: false, + }) + .returning(); +``` + +### Updating + +```ts +await db + .update(events) + .set({ title: 'Updated Title', isPublished: true }) + .where( + and( + eq(events.id, eventId), + eq(events.siteId, locals.site.id) // NEVER forget this! + ) + ); +``` + +## Migration Strategy + +1. **Additive changes only** in production — new columns, new tables. Avoid renames or destructive changes. +2. **Make schema changes in `schema.ts` first**, then generate the migration: + ```bash + npx drizzle-kit generate + ``` +3. **Apply via the primary deployment** (the one with `RUN_MIGRATIONS=true`) +4. **JSON columns for settings** reduce migration frequency for feature additions +5. **Seed script** at [`scripts/seed.mjs`](scripts/seed.mjs) for local dev setup + +## What This Schema Intentionally Avoids (V1) + +- **No `accounts` or `sessions` tables** — Better Auth manages those +- **No `pages` table** — homepage content lives in `siteSettings.homepage` JSON +- **No `reviews`, `comments`, `posts` tables** — future phases +- **No `featureFlags` table** — use env vars or settings JSON +- **No `domains` table** — single `SITE_SLUG` resolution in V1 +- **No `auditLog` table** — future phase +- **No `invitations` table** — owner adds admins directly diff --git a/.github/instructions/deployment-guide.instructions.md b/.github/instructions/deployment-guide.instructions.md new file mode 100644 index 0000000..c633470 --- /dev/null +++ b/.github/instructions/deployment-guide.instructions.md @@ -0,0 +1,132 @@ +--- +description: 'Use when deploying a new site instance, setting up Coolify, configuring Docker, managing migrations, or troubleshooting deployment issues for The Collective Hub.' +applyTo: 'Dockerfile', 'docker-compose.yml', '.dockerignore', 'Coolify' +--- + +# Deployment Guide + +## Deployment Model + +The Collective Hub uses **multiple Coolify deployments** from a single Git repository. Each deployment runs the same Docker image but is configured with different environment variables — most importantly, a different `SITE_SLUG`. + +``` +Git Repo (main branch) + │ + ├── Coolify Deployment "bad-movies-theater" + │ ├── SITE_SLUG=bad-movies-theater + │ ├── PUBLIC_SITE_URL=https://badmovies.example.com + │ ├── OWNER_DISCORD_ID=... (site owner) + │ └── RUN_MIGRATIONS=false + │ + ├── Coolify Deployment "garbage-day" + │ ├── SITE_SLUG=garbage-day + │ ├── PUBLIC_SITE_URL=https://garbageday.example.com + │ ├── OWNER_DISCORD_ID=... (site owner) + │ └── RUN_MIGRATIONS=false + │ + └── Coolify Deployment "primary" (migration runner) + ├── SITE_SLUG=primary + ├── PUBLIC_SITE_URL=https://primary.example.com + ├── OWNER_DISCORD_ID=... (David's Discord ID) + └── RUN_MIGRATIONS=true +``` + +## Docker + +The project uses a multi-stage Docker build (see [`Dockerfile`](Dockerfile)): + +```dockerfile +# Stage 1: Build +FROM node:22-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Stage 2: Production +FROM node:22-alpine AS runner +WORKDIR /app +COPY --from=builder /app/build build/ +COPY --from=builder /app/node_modules node_modules/ +COPY package.json . +EXPOSE 3000 +CMD ["node", "build"] +``` + +### Local Development with Docker Compose + +See [`docker-compose.yml`](docker-compose.yml) for local development setup, which includes: +- Postgres database container +- The SvelteKit app container +- Proper environment variables for local dev + +```bash +docker compose up +``` + +## Adding a New Site + +To add a new community/theater site: + +1. **Insert a site row in the database:** + ```sql + INSERT INTO sites (slug, name) VALUES ('new-community', 'New Community'); + INSERT INTO site_settings (site_id, settings) VALUES ( + (SELECT id FROM sites WHERE slug = 'new-community'), + '{"branding":{"siteName":"New Community","tagline":"Coming soon"},"theme":{"preset":"dark","accentColor":"#e63946","backgroundColor":"#1a1a2e","textColor":"#eaeaea"},"homepage":{"heroTitle":"Welcome","heroSubtitle":"","aboutText":"","primaryButtonText":"Join us on Discord","primaryButtonLink":"","showNextEvent":true,"showSchedule":true},"layout":{"preset":"standard"}}' + ); + ``` + Or use the seed script pattern. + +2. **Create a new Coolify deployment:** + - Point to the same Git repo + branch + - Set `SITE_SLUG=new-community` + - Set `PUBLIC_SITE_URL=https://newcommunity.example.com` + - Set `OWNER_DISCORD_ID` to the site owner's Discord user ID + - Set `RUN_MIGRATIONS=false` (unless this is the designated migration runner) + +3. **Configure DNS:** Point the domain to the Coolify deployment's IP/URL + +## Environment Variables Per Deployment + +Each deployment needs its own set of env vars. See [`docs/04-environment-variables.md`](docs/04-environment-variables.md) for the full reference. + +**Rules:** +- Shared vars (`DATABASE_URL`, `DISCORD_CLIENT_ID`, `CDN_BASE_URL`) must be identical across all deployments +- Per-site vars (`SITE_SLUG`, `PUBLIC_SITE_URL`, `OWNER_DISCORD_ID`) are unique per deployment +- `RUN_MIGRATIONS=true` on exactly one deployment (the primary/migration runner) + +## Migration Runner + +The migration runner deployment is critical: + +- It runs `RUN_MIGRATIONS=true` and executes schema migrations on startup +- It must be deployed **first** when schema changes are included in a release +- Other deployments should be deployed after the migration completes +- If the migration runner is down, other deployments still serve traffic (they just won't have schema changes) + +## Coolify Configuration + +In Coolify, each deployment: +- Uses the **same Git repository** and branch +- Has its own **unique environment variables** +- Can have its own **domain/SSL configuration** +- Is deployed independently (zero-downtime per deployment) + +### Deploy Strategy + +1. Deploy the migration runner first (`RUN_MIGRATIONS=true`) +2. Wait for it to be healthy (migrations complete) +3. Deploy other sites in any order +4. Each deployment is independent — one failing doesn't affect others + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| "Site not found" on load | No matching row in `sites` table for `SITE_SLUG` | Insert the site row or fix `SITE_SLUG` env var | +| Login redirects to wrong URL | `PUBLIC_SITE_URL` / `BETTER_AUTH_URL` mismatch | Ensure both match the deployment's actual URL | +| "Already exists" errors on deploy | Two deployments running migrations simultaneously | Check only one has `RUN_MIGRATIONS=true` | +| Images not loading | `CDN_BASE_URL` missing or wrong, or `cdnKey` starts with `/` | Ensure no leading slash on CDN keys | +| Auth callbacks failing | Discord OAuth redirect URI doesn't match deployment URL | Add the correct URL to Discord Developer Portal | diff --git a/.github/instructions/docs-workflow.instructions.md b/.github/instructions/docs-workflow.instructions.md new file mode 100644 index 0000000..76fd4b2 --- /dev/null +++ b/.github/instructions/docs-workflow.instructions.md @@ -0,0 +1,110 @@ +--- +description: 'Use when maintaining project documentation in the docs/ directory. Covers guidelines for updating docs when code changes, keeping docs in sync with implementation, when to create new docs vs update existing ones, and the documentation review process for The Collective Hub.' +applyTo: 'docs/*.md' +--- + +# Documentation Workflow + +> This instruction file covers conventions for maintaining the project documentation in [`docs/`](docs/). The Collective Hub's documentation is technical project documentation — architecture plans, database schemas, UX plans, environment variable references, and development roadmaps. + +## Overview + +The `docs/` directory contains the canonical technical reference for The Collective Hub: + +| File | Purpose | +| ---- | ------- | +| [`00-project-brief.md`](docs/00-project-brief.md) | Project overview — what The Collective Hub is and who it's for | +| [`01-architecture-plan.md`](docs/01-architecture-plan.md) | Technical architecture — stack, site resolution, deployment model | +| [`02-database-plan.md`](docs/02-database-plan.md) | Database schema — all tables, relationships, migration strategy | +| [`03-feature-roadmap.md`](docs/03-feature-roadmap.md) | Feature phases — Phase 1 (Foundation) through Phase 4 | +| [`04-environment-variables.md`](docs/04-environment-variables.md) | Environment variable reference — shared vs per-site | +| [`05-admin-ux-plan.md`](docs/05-admin-ux-plan.md) | Admin panel UX — layout, navigation, settings pages | +| [`06-public-site-ux-plan.md`](docs/06-public-site-ux-plan.md) | Public site UX — hero, about, events, social links, footer | +| [`07-development-plan.md`](docs/07-development-plan.md) | Development workflow — git flow, Docker, testing, deployment | +| [`08-open-questions.md`](docs/08-open-questions.md) | Open questions — decisions deferred for later resolution | +| [`09-risks-and-rules.md`](docs/09-risks-and-rules.md) | Critical risks — migration conflicts, data leaks, deployment strategy | + +## When to Update Documentation + +### Code Changes That Require a Docs Update + +Update the relevant `docs/*.md` file when making code changes that affect: + +- **Database schema** — new tables, columns, indexes, or relationship changes → update [`02-database-plan.md`](docs/02-database-plan.md) and [`database-schema.instructions.md`](.github/instructions/database-schema.instructions.md) +- **Architecture** — site resolution flow, env vars, deployment model → update [`01-architecture-plan.md`](docs/01-architecture-plan.md) and [`04-environment-variables.md`](docs/04-environment-variables.md) +- **Environment variables** — new required env vars → update [`04-environment-variables.md`](docs/04-environment-variables.md) and the `.env` stub in [`ci-web.yml`](.github/workflows/ci-web.yml) +- **Admin panel** — new admin pages, changed settings flow → update [`05-admin-ux-plan.md`](docs/05-admin-ux-plan.md) and [`admin-panel.instructions.md`](.github/instructions/admin-panel.instructions.md) +- **Public site** — new sections, changed rendering logic → update [`06-public-site-ux-plan.md`](docs/06-public-site-ux-plan.md) and [`public-site-theming.instructions.md`](.github/instructions/public-site-theming.instructions.md) +- **Feature roadmap** — new features, changed priorities, phase shifts → update [`03-feature-roadmap.md`](docs/03-feature-roadmap.md) +- **Risks** — newly discovered risks or changed mitigations → update [`09-risks-and-rules.md`](docs/09-risks-and-rules.md) + +### When to Update vs Create + +| Situation | Action | +| --------- | ------ | +| A feature or configuration covered by an existing doc changes | **Update** the existing file | +| A new aspect of the project needs documentation that fits an existing doc | **Update** the existing file with a new section | +| A fundamentally new concern emerges that doesn't fit any existing doc | **Create** a new `docs/XX-topic.md` file and add it to the table in AGENTS.md and this file | +| A code change is purely internal refactoring with no architectural impact | **No docs update needed** | + +## Documentation Standards + +### Format + +- All docs are Markdown (`.md`) files with GitHub-flavored Markdown +- Use ATX headings (`#`, `##`, `###`) — no Setext headings +- Include a table of contents for files longer than ~100 lines +- Use relative links to reference other docs and code files +- Code blocks should specify the language for syntax highlighting + +### Cross-Referencing + +- Reference tech stack instruction files in `.github/instructions/` where relevant +- Reference code paths with links to the actual file (e.g., [`site-resolver.ts`](src/lib/server/site-resolver.ts)) +- Keep the file reference index in [`AGENTS.md`](AGENTS.md) up to date when adding new documentation + +### Style + +- Write for **David (system maintainer)** as the primary audience — assume technical competence but need clarity +- Be **specific and concise** — avoid marketing language, focus on technical accuracy +- Use **tables** for structured information (env vars, routes, schema columns) +- Use **bullet lists** for unordered items, **numbered lists** for sequential steps +- Include **example code** where it clarifies usage + +## Documentation Review Process + +### During Development + +1. **Self-review**: When implementing a feature, check whether any `docs/*.md` files need updating +2. **Co-located changes**: Include documentation updates in the same PR/commit as the code change — never in a separate "update docs" pass +3. **Cross-reference check**: After updating a doc, verify all cross-references still resolve (file paths, section anchors) + +### Before Merge + +1. **Freshness check**: Does the doc still reflect the current state of the code? +2. **Accuracy check**: Are all code examples, SQL queries, and configuration snippets up to date? +3. **Completeness check**: Does the doc cover edge cases and error states, or just the happy path? +4. **Link check**: Do all relative links resolve correctly? + +### Periodic Maintenance + +- When adding a new feature from the roadmap ([`03-feature-roadmap.md`](docs/03-feature-roadmap.md)), review all docs that reference the affected system +- After a deployment involving schema changes, verify [`02-database-plan.md`](docs/02-database-plan.md) matches the actual database state +- When a risk in [`09-risks-and-rules.md`](docs/09-risks-and-rules.md) materializes or is mitigated, update it promptly + +## Related Instruction Files + +The following instruction files in `.github/instructions/` provide detailed conventions that complement the project documentation: + +| File | Focus | +| ---- | ----- | +| [`multi-tenant-architecture.instructions.md`](.github/instructions/multi-tenant-architecture.instructions.md) | Site resolution, data scoping, deployment model | +| [`database-schema.instructions.md`](.github/instructions/database-schema.instructions.md) | All tables, relationships, migration strategy | +| [`auth-and-roles.instructions.md`](.github/instructions/auth-and-roles.instructions.md) | Better Auth, Discord OAuth, role system | +| [`deployment-guide.instructions.md`](.github/instructions/deployment-guide.instructions.md) | Coolify multi-deploy, Docker, migration runner | +| [`admin-panel.instructions.md`](.github/instructions/admin-panel.instructions.md) | Admin layout, auth guards, form patterns | +| [`cdn-and-assets.instructions.md`](.github/instructions/cdn-and-assets.instructions.md) | CDN helpers, image upload, webp conversion | +| [`public-site-theming.instructions.md`](.github/instructions/public-site-theming.instructions.md) | SSR landing page, CSS custom properties | +| [`api-route-patterns.instructions.md`](.github/instructions/api-route-patterns.instructions.md) | API route conventions, validation, site scoping | +| [`testing-multi-tenant.instructions.md`](.github/instructions/testing-multi-tenant.instructions.md) | Multi-tenant test patterns, mocking, auth testing | +| [`server-ts.instructions.md`](.github/instructions/server-ts.instructions.md) | Server-side TypeScript patterns, Drizzle queries | diff --git a/.github/instructions/icons.instructions.md b/.github/instructions/icons.instructions.md new file mode 100644 index 0000000..1e820c6 --- /dev/null +++ b/.github/instructions/icons.instructions.md @@ -0,0 +1,186 @@ +--- +description: "Use when adding icons to components or pages for The Collective Hub. Covers @lucide/svelte usage patterns — import, pass, style, and render icons." +applyTo: "src/**/*.svelte", "src/**/*.ts" +--- + +# Icon Usage + +## Overview + +This project uses Lucide Icons for all UI iconography: + +| Source | Package | Purpose | +| ---------------- | --------------------------------------------- | -------------------------------------------------------------- | +| **Lucide Icons** | [`@lucide/svelte`](https://lucide.dev/icons/) | General UI icons (arrows, actions, status, social media, etc.) | + +--- + +## 1. Lucide Icons (`@lucide/svelte`) + +### Import Pattern + +Import individual icons as named exports from `@lucide/svelte` — there is no centralized icon barrel/index file: + +```ts +import { Heart, Menu, X, BookOpen, Video, Star, ThumbsUp } from '@lucide/svelte'; +``` + +Browse the full icon catalog at [lucide.dev/icons](https://lucide.dev/icons/). + +> **Note:** There is no `$lib/components/ui/icons` barrel. All icon imports are direct from `@lucide/svelte`. If a centralized re-export pattern is desired in the future, it should be added to `src/lib/icons/` (which does not exist yet) and clearly marked as planned in this document. + +### Component Props + +Each Lucide icon component accepts these props: + +| Prop | Type | Default | Description | +| ------------- | -------- | ------- | -------------------------------- | +| `size` | `number` | `24` | Width & height in pixels | +| `class` | `string` | `''` | Tailwind CSS classes for styling | +| `strokeWidth` | `number` | `2` | Stroke width of the icon paths | + +### Rendering + +Use icons as self-closing components in the template: + +```svelte + + +