init repo

This commit is contained in:
JCEEE
2026-06-23 11:27:05 +01:00
commit 2253b54077
37 changed files with 2382 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}
+106
View File
@@ -0,0 +1,106 @@
## Project Configuration
- **Language**: TypeScript
- **Package Manager**: pnpm
- **Add-ons**: prettier, tailwindcss, sveltekit-adapter, experimental
---
# FamChore v2 — AI Agent Reference
## Stack
- SvelteKit (SSR frontend) + Hono proxy (same container, port :3001)
- PocketBase (separate Coolify service at `pb.chores.app.com`, :8090)
- Stripe one-time donations
- Coolify CRON → `GET /api/weekly-cron`
- Deployment: Coolify, Cloudflare DNS
## Auth
| Role | Auth | Session |
|---|---|---|
| Super admin (me) | PB email+pass | 24hr JWT |
| Fam admin | PB email+pass | 24hr JWT, scoped to own fam |
| Member | Invite code + device token | localStorage, no expiry |
## PB Collections (all scoped by `famId`)
- `fams` — name, slug, inviteCode, stripeCustomerId, featureFlags
- `members` — famId, name, color, deviceToken(hashed), deviceTokenHint
- `chore_templates` — famId, name, defaultValue, defaultFrequency
- `assigned_chores` — famId, memberId, templateId, frequency, value
- `completions` — famId, memberId, assignedChoreId, date
- `weekly_history` — famId, memberId, weekStart, pointsEarned, moneyEarned
- `rewards` — famId, memberId, source, label, value, claimed, claimedAt
- `monthly_bonuses` — famId, month, prizeType, prizeValue, winnerMemberId
- `settings` — famId, pointsThreshold, weeklyBonus, webhookUrl
## Routes
```
/ Landing (SaaS marketing)
/admin Admin panel - statistic dashboard, and any donations made
/join/:code Member invite code
/{fam} Fam dashboard
/{fam}/admin Admin panel
/{fam}/:username Member kanban (?token= for auth)
/api/* Hono proxy (webhooks, CRON)
```
## Data Flow
- **Chore toggle:** Browser → PB SDK directly (auth via device token or admin JWT)
- **Admin CRUD:** Browser → PB SDK (admin JWT)
- **Reward creation:** After completion toggle, SvelteKit server creates reward if threshold met
- **Weekly CRON:** Coolify → `GET /api/weekly-cron` on Hono → Hono queries PB, computes summaries, upserts weekly_history
- **Strip donate:** Browser → Hono `/api/stripe/create-checkout` → Stripe → Hono webhook → update fam
- **WhatsApp:** Deferred — Hono CRON handler has pluggable notification interface
## Conventions
- Every collection query includes `famId = @request.auth.famId` filter
- Super admin bypasses famId filter (access via PB admin API)
- `deviceToken` stored as SHA-256 hash; never log raw tokens
- Environment: `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD`, `STRIPE_SECRET_KEY`, `DONATION_MODAL_INTERVAL`
- Seed via JSON dump (portable for dev)
- Monorepo: SvelteKit in `/src`, Hono in `/proxy`, two Dockerfiles
## Build Phases (must validate each before next)
### Phase 1 — Infrastructure
1.1 Scaffold SvelteKit + Hono monorepo
1.2 Write Dockerfiles (frontend + backend, correct port mapping)
1.3 Deploy PocketBase on Coolify → validate data persists
1.4 Deploy SvelteKit+Hono on Coolify → validate routing
1.5 Validate Hono `/api/*` reachable, env vars injected
1.6 Validate SvelteKit↔PB connectivity (admin API read/write)
### Phase 2 — Backend Core
2.1 Create PB collections via schema/migration
2.2 Super admin seed + fam signup flow
2.3 Fam admin login (email/pass → 24hr JWT)
2.4 Invite code generation + member join flow
2.5 Device token auth + route guards
2.6 PB realtime SSE subscriptions from FE
2.7 Remote functions (PB SDK client helpers)
### Phase 3 — Backend Data Streams
3.1 Chore template CRUD + assignment grid (admin)
3.2 Completion toggle (member → PB direct)
3.3 Weekly progress + history computation
3.4 Reward auto-creation on threshold
3.5 Reward claim flow + admin CRUD
3.6 Monthly bonus evaluation
3.7 CRON handler (Coolify → Hono)
3.8 Stripe checkout + webhook
3.9 Notification interface (WhatsApp deferred)
### Phase 4 — Frontend App
4.1 Member kanban (3-column, live SSE updates)
4.2 Admin dashboard (weekly overview, chart)
4.3 Admin panel (members, chores, rewards, settings)
4.4 Landing page (SaaS marketing)
4.5 Super admin stats dashboard
4.6 Donation modal
4.7 QR invite code
4.8 Polish (loading, empty, error states, responsive)
+385
View File
@@ -0,0 +1,385 @@
# FamChore v2 — Architecture & Developer Reference
## Overview
Multi-tenant chore tracking SaaS. Families ("fams") are isolated tenant groups. Fam admins use email/password. Members join via invite code + device token (no password). Super admin (you) can see everything.
**Demo reference:** Current prototype at `/home/threejjjs/development/famchore/`
---
## 1. Stack
| Component | Role | Deploy | Port |
|---|---|---|---|
| SvelteKit | SSR frontend, all UI | Coolify Docker (chores.app.com) | :3000 |
| Hono proxy | Stripe, CRON, webhooks | Same container as SvelteKit, proxied via `/api/*` | :3001 (internal) |
| PocketBase | DB, auth, realtime, storage, Admin UI | Coolify Docker (pb.chores.app.com) | :8090 |
### Deployment Topology
```
chores.app.com ────┬──► SvelteKit (:3000)
│ └── /api/* ──► Hono proxy (:3001)
pb.chores.app.com ──► PocketBase (:8090)
│ Admin UI at /_
│ Volume: /pb_data (persistence + backups)
stripe.com ─────────► Hono /api/stripe/webhook
```
---
## 2. Auth Model
### 2.1 Roles & Methods
| Role | Auth | Session | Scope | PB Entity |
|---|---|---|---|---|
| **Super admin** (you) | PB email + password | 24hr JWT | All collections, all fams | PB `users` (set via seed) |
| **Fam admin** | PB email + password | 24hr JWT | Their fam only | PB `users` |
| **Member** | Invite code + device token | localStorage, no expiry | Own data only | `members` collection |
### 2.2 Member Auth Flow
1. Fam admin generates invite code → stored on `fams.inviteCode`
2. Admin shares as text (`chores.app.com/join/XYZ123`) or QR code
3. Member opens link, enters name, browser generates `crypto.randomUUID()` as device token
4. PB API creates `members` record with SHA-256 hashed token
5. Device token stored in `localStorage`, sent as `X-Device-Token` header
6. If token lost → admin regenerates invite code, member re-registers
7. Admin can revoke access by deleting member record
### 2.3 PB Auth Rules (row-level security)
Every collection has `famId` field. PB rule pattern:
```
famId = @request.auth.famId
```
Super admin bypasses via admin API (PB superuser credentials).
---
## 3. Data Model
All collections live in PocketBase. Every tenant-scoped collection includes `famId` (relation to `fams`).
### `fams`
| Field | Type | Notes |
|---|---|---|
| `id` | auto | PB default |
| `name` | text | Display name |
| `slug` | text | URL segment, unique |
| `inviteCode` | text | Short alphanumeric, regeneratable |
| `stripeCustomerId` | text? | Set after first donation |
| `featureFlags` | json | `{ "monthlyBonus": true }` |
| `created` | auto | |
### `members`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `name` | text | |
| `color` | text | Hex |
| `deviceToken` | text | SHA-256 hash of raw token |
| `deviceTokenHint` | text | First 8 chars of raw token (for admin display) |
| `pointsThreshold` | number? | Override fam default |
| `weeklyBonus` | number? | Override fam default |
| `created` | auto | |
### `chore_templates`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `name` | text | |
| `description` | text | |
| `defaultFrequency` | select | `daily` or `weekly` |
| `defaultType` | select | `points` or `money` |
| `defaultValue` | number | |
### `assigned_chores`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `memberId` | relation→members | |
| `templateId` | relation→chore_templates | |
| `frequency` | select | |
| `type` | select | |
| `value` | number | |
| `customName` | text? | |
### `completions`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `memberId` | relation→members | |
| `assignedChoreId` | relation→assigned_chores | |
| `date` | date | ISO date |
| `completedAt` | auto | |
### `rewards`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `memberId` | relation→members | |
| `source` | select | `weekly_bonus`, `monthly_bonus`, `custom` |
| `label` | text | |
| `value` | number | |
| `weekStart` | date? | |
| `month` | text? | "2026-06" |
| `claimed` | bool | |
| `claimedAt` | auto? | |
### `weekly_history`
| Field | Type |
|---|---|
| `famId` | relation→fams |
| `memberId` | relation→members |
| `weekStart` | date |
| `pointsEarned` | number |
| `moneyEarned` | number |
| `choresCompleted` | number |
| `bonusEarned` | number |
### `monthly_bonuses`
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | |
| `month` | text | "2026-06" |
| `prizeType` | select | `cash`, `string` |
| `prizeValue` | text | |
| `winnerMemberId` | relation→members? | Nullable until computed |
| `pointsScored` | number? | |
| `claimed` | bool | |
### `settings` (singleton per fam)
| Field | Type | Notes |
|---|---|---|
| `famId` | relation→fams | Unique |
| `pointsThreshold` | number | Default: 100 |
| `weeklyBonus` | number | Default: 2 (e.g. £2) |
| `webhookUrl` | text? | N8N/notification URL |
---
## 4. Routes
### SvelteKit
```
/ Landing page (SaaS marketing)
/join/:code Member invite code + name entry
/{fam} Fam dashboard (weekly overview)
/{fam}/admin Admin panel
/{fam}/admin/chores Chore template CRUD + assignment grid
/{fam}/admin/rewards Reward management, claim history
/{fam}/admin/settings Thresholds, webhook, invite code, features
/{fam}/:username Member kanban
?token=<deviceToken> Auto-auth via query param (from QR/share)
```
### Hono proxy (`/api/*`)
```
/api/stripe/create-checkout Create Stripe Checkout Session
/api/stripe/webhook Stripe event webhook
/api/weekly-cron Coolify CRON target
```
---
## 5. Data Flow
### 5.1 Chore Toggle
```
User clicks checkbox
→ Browser → PB SDK (direct, auth= device token or admin JWT)
→ PB inserts/deletes completion
→ PB realtime SSE push to all subscribers
→ UI updates (kanban card, progress bar, money counter)
→ If weekly threshold crossed:
→ SvelteKit server handler creates Reward (weekly_bonus)
```
### 5.2 Weekly CRON
Triggered by Coolify CRON job → `GET /api/weekly-cron`:
```
Hono receives request
→ Queries all fams
→ For each fam:
→ Compute weekly summaries per member (completions tally)
→ Upsert weekly_history records
→ Evaluate monthly bonus (end-of-month)
→ If webhookUrl set on fam settings:
→ POST summary to webhook (pluggable — WhatsApp later)
→ Returns 200
```
### 5.3 Stripe Donation
```
User clicks "Donate" on landing or modal
→ Hono /api/stripe/create-checkout
→ Creates Stripe Checkout Session
→ Returns session.url → redirect user to Stripe
→ Stripe redirects back to app
→ Stripe webhook → Hono /api/stripe/webhook
→ Updates fam.stripeCustomerId
→ Sets fam.featureFlags.donated = true (or similar)
```
### 5.4 Admin CRUD
All admin operations go through PB admin API (Hono proxy or `+page.server.ts`). This ensures:
- Server-side validation of famId
- Audit trail option
- Consistent error handling
---
## 6. Project Structure
```
/chores
/src SvelteKit app
/lib
/components Shared UI components
/stores Svelte stores (auth, current fam)
/pb PB SDK client helpers
/routes SvelteKit file-based routing
/hooks.server.ts Auth hooks, PB client init
/proxy Hono proxy
/src
/routes Stripe webhook, CRON handler
/services PB admin client, notification service
package.json
/seed JSON dump files for PB collections
/pb PB collection schema definitions
package.json Workspace root
Dockerfile.frontend SvelteKit + Hono build
Dockerfile.backend PocketBase (custom, or use official image)
coolify.json Coolify deployment config (optional)
AGENTS.md AI reference (this file's sibling)
ARCHITECTURE.md This document
```
---
## 7. Environment Variables
### Frontend + Hono container
```
PUBLIC_PB_URL=https://pb.chores.app.com
PB_ADMIN_EMAIL=admin@chores.app
PB_ADMIN_PASSWORD=<super-admin-password>
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
DONATION_MODAL_INTERVAL=30
```
### PocketBase container
```
PB_SUPERUSER_EMAIL=you@email.com
PB_SUPERUSER_PASSWORD=<your-password>
```
---
## 8. Implementation Phases
### Phase 0 — Infrastructure (2-4 hrs)
- [ ] Deploy PocketBase on Coolify (`pb.chores.app.com`)
- Official `pocketbase/pocketbase` image
- Mount `/pb_data` volume
- Set super admin env vars
- Test: visit `pb.chores.app.com/_/`, login, data persists after restart
- [ ] Scaffold monorepo with SvelteKit + Hono
- [ ] Create Dockerfiles
- [ ] Deploy to Coolify (`chores.app.com`)
- [ ] Cloudflare DNS for both
### Phase 1 — Auth (4-6 hrs)
- [ ] Create PB collections: `fams`, `members`, `settings`
- [ ] Super admin seed script
- [ ] Fam signup → PB user + fam record
- [ ] Fam admin login (24hr JWT)
- [ ] Invite code generation
- [ ] Member join page (`/join/:code`)
- [ ] Device token auth
- [ ] Route guards
- [ ] Seed data (1 fam + 3 members + chores matching demo)
### Phase 2 — Core Chore Tracking (6-8 hrs)
- [ ] PB collections: `chore_templates`, `assigned_chores`, `completions`, `weekly_history`
- [ ] Admin chore CRUD
- [ ] Admin chore assignment grid
- [ ] Member kanban (3 columns: Daily Pending, Weekly Pending, Completed)
- [ ] Completion toggle
- [ ] Realtime updates (PB SSE)
- [ ] Weekly progress + chart
- [ ] Admin dashboard
### Phase 3 — Rewards + Claims (3-4 hrs)
- [ ] PB collection: `rewards`
- [ ] Auto-create weekly bonus reward on threshold
- [ ] Claim section with visual feedback
- [ ] Admin reward CRUD
- [ ] Monthly bonus (set prize → compute winner → create reward)
- [ ] Pluggable notification interface in CRON handler
### Phase 4 — SaaS (4-6 hrs)
- [ ] Landing page (hero, features, CTA)
- [ ] Stripe one-time checkout (Hono)
- [ ] Donation modal (triggered after N admin page loads)
- [ ] Feature flags (checked in routes via settings)
- [ ] Super admin dashboard (stats, all-fams view, feature toggles)
### Phase 5 — Polish (ongoing)
- [ ] QR invite code
- [ ] PB backup scheduler
- [ ] Error/loading/empty states
- [ ] Responsive mobile layout
- [ ] Accessibility
---
## 9. Key Conventions
- **`famId` on every query** — PB auth rules enforce `famId = @request.auth.famId`
- **PB SDK client-side for members** — Browser talks to PB directly for toggles, reads. PB auth rules handle security.
- **PB admin API server-side for admins** — Hono proxy or SvelteKit server handlers for admin CRUD.
- **Device tokens as SHA-256** — Never store or log raw tokens.
- **JSON dump for seed data** — Portable, version-controllable, restorable via PB backup CLI.
- **All collection schema files in `/pb/`** — Tracked in git, used for CI/CD schema migration.
- **Notifications via pluggable interface** — Hono CRON handler has `NotificationService` interface; WhatsApp is one implementation (deferred).
---
## 10. Open / Deferred
- **WhatsApp notifications** — Will be implemented as a `NotificationService` plugin for the weekly CRON handler. N8N or Twilio, TBD.
- **Subscription payments** — Currently one-time donation only. Subscription model can be added later via Stripe webhooks.
- **Member re-auth on new device** — Current design requires admin to regenerate invite code. Could add "re-issue link" feature in admin panel.
- **Multi-language** — Not yet scoped. All text in English for now.
---
## 11. Reference: Current Prototype
The existing HonoJS prototype at `/home/threejjjs/development/famchore/` contains the reference logic for:
- Weekly bonus calculation (`src/services.ts``checkWeeklyBonus`)
- Monthly bonus evaluation
- Reward claim flow
- Chore toggle event delegation
- Chart/stat formatting
- Date/timezone helpers
Refer to `src/types.ts` for the original type definitions, and `src/views/` for the Alpine.js template structure that maps to the new SvelteKit components.
+42
View File
@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
pnpm dlx sv@0.16.1 create --template minimal --types ts --add prettier tailwindcss="plugins:none" sveltekit-adapter="adapter:node" experimental="versions:kit+features:async,remoteFunctions,explicitEnvironmentVariables,handleRenderingErrors" --install pnpm .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+1
View File
@@ -0,0 +1 @@
export const PROXY_PORT = 3456;
+9
View File
@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}
+3
View File
@@ -0,0 +1,3 @@
// this file is generated — do not edit it
/// <reference types="@sveltejs/kit" />
+9
View File
@@ -0,0 +1,9 @@
// See https://svelte.dev/docs/kit/environment-variables for more information
declare module '$app/env/private' {
// no private environment variables were defined
}
declare module '$app/env/public' {
// no public environment variables were defined
}
@@ -0,0 +1,29 @@
export { matchers } from './matchers.js';
export const nodes = [
() => import('./nodes/0'),
() => import('./nodes/1'),
() => import('./nodes/2')
];
export const server_loads = [];
export const dictionary = {
"/": [2]
};
export const hooks = {
handleError: (({ error }) => { console.error(error) }),
reroute: (() => {}),
transport: {}
};
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
export const hash = false;
export const decode = (type, value) => decoders[type](value);
export { default as root } from '../root.js';
@@ -0,0 +1 @@
export const matchers = {};
@@ -0,0 +1 @@
export { default as component } from "../../../../src/routes/+layout.svelte";
@@ -0,0 +1 @@
export { default as component } from "../../../../node_modules/.pnpm/@sveltejs+kit@3.0.0-next.4_@sveltejs+vite-plugin-svelte@7.1.2_svelte@5.56.3_vite@8.0.16_8dfd504a6c74c36e5d149ff76188a39d/node_modules/@sveltejs/kit/src/runtime/components/error.svelte";
@@ -0,0 +1 @@
export { default as component } from "../../../../src/routes/+page.svelte";
+3
View File
@@ -0,0 +1,3 @@
import { asClassComponent } from 'svelte/legacy';
import Root from './root.svelte';
export default asClassComponent(Root);
@@ -0,0 +1,75 @@
<!-- This file is generated by @sveltejs/kit — do not edit it! -->
<svelte:options runes={true} />
<script>
import { setContext, onMount, tick } from 'svelte';
import { browser } from '$app/env';
// stores
let { stores, page, constructors, components = [], form, errors = [], error, data_0 = null, data_1 = null } = $props();
let data = $derived({'0': data_0, '1': data_1})
if (browser) {
$effect.pre(() => stores.page.set(page));
} else {
// svelte-ignore state_referenced_locally
setContext('__svelte__', stores);
// svelte-ignore state_referenced_locally
stores.page.set(page);
}
$effect(() => {
stores;page;constructors;components;form;errors;error;data_0;data_1;
stores.page.notify();
});
let mounted = $state(false);
let navigated = $state(false);
let title = $state(null);
onMount(() => {
const unsubscribe = stores.page.subscribe(() => {
if (mounted) {
navigated = true;
tick().then(() => {
title = document.title || 'untitled page';
});
}
});
mounted = true;
return unsubscribe;
});
const Pyramid_1 = $derived(constructors[1]);
</script>
{#snippet pyramid(depth)}
{@const Pyramid = constructors[depth]}
{#snippet failed(error)}
{@const ErrorPage = errors[depth]}
<ErrorPage {error} />
{/snippet}
<svelte:boundary failed={errors[depth] ? failed : undefined}>
{#if constructors[depth + 1]}
{@const d = data[depth]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params}>
{@render pyramid(depth + 1)}
</Pyramid>
{:else}
{@const d = data[depth]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params} {error} />
{/if}
</svelte:boundary>
{/snippet}
{@render pyramid(0)}
{#if mounted}
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
{#if navigated}
{title}
{/if}
</div>
{/if}
@@ -0,0 +1,52 @@
import root from '../root.js';
import { set_building, set_prerendering } from '$app/env/internal';
import { set_assets } from '$app/paths/internal/server';
import { set_manifest, set_read_implementation } from '__sveltekit/server';
import { set_env } from '__sveltekit/env';
export const options = {
app_template_contains_nonce: false,
async: true,
csp: {"mode":"auto","directives":{"upgrade-insecure-requests":false,"block-all-mixed-content":false},"reportOnly":{"upgrade-insecure-requests":false,"block-all-mixed-content":false}},
csrf_check_origin: true,
csrf_trusted_origins: [],
embedded: false,
hash_routing: false,
hooks: null, // added lazily, via `get_hooks`
link_header_preload: false,
root,
service_worker: false,
service_worker_options: undefined,
server_error_boundaries: true,
templates: {
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta name=\"text-scale\" content=\"scale\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
},
version_hash: "3gjt3w"
};
export async function get_hooks() {
let handle;
let handleFetch;
let handleError;
let handleValidationError;
let init;
let reroute;
let transport;
return {
handle,
handleFetch,
handleError,
handleValidationError,
init,
reroute,
transport
};
}
export { set_assets, set_building, set_env, set_manifest, set_prerendering, set_read_implementation };
+43
View File
@@ -0,0 +1,43 @@
// this file is generated — do not edit it
declare module "svelte/elements" {
export interface HTMLAttributes<T> {
'data-sveltekit-keepfocus'?: true | false | '' | undefined | null;
'data-sveltekit-noscroll'?: true | false | '' | undefined | null;
'data-sveltekit-preload-code'?:
| true
| false
| ''
| 'eager'
| 'viewport'
| 'hover'
| 'tap'
| undefined
| null;
'data-sveltekit-preload-data'?: true | false | '' | 'hover' | 'tap' | undefined | null;
'data-sveltekit-reload'?: true | false | '' | undefined | null;
'data-sveltekit-replacestate'?: true | false | '' | undefined | null;
}
}
export {};
declare module "$app/types" {
type MatcherParam<M> = M extends (param : string) => param is (infer U extends string) ? U : string;
export interface AppTypes {
RouteId(): "/";
RouteParams(): {
};
LayoutParams(): {
"/": Record<string, never>
};
Pathname(): "/";
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
Asset(): "/robots.txt" | string & {};
}
}
+60
View File
@@ -0,0 +1,60 @@
{
"compilerOptions": {
"paths": {
"$lib": [
"../src/lib"
],
"$lib/*": [
"../src/lib/*"
],
"$app/types": [
"./types/index.d.ts"
]
},
"rootDirs": [
"..",
"./types"
],
"verbatimModuleSyntax": true,
"isolatedModules": true,
"lib": [
"esnext",
"DOM",
"DOM.Iterable"
],
"moduleResolution": "bundler",
"module": "esnext",
"noEmit": true,
"target": "esnext",
"types": [
"node"
]
},
"include": [
"ambient.d.ts",
"env.d.ts",
"non-ambient.d.ts",
"./types/**/$types.d.ts",
"../svelte.config.js",
"../vite.config.js",
"../vite.config.ts",
"../src/**/*.js",
"../src/**/*.ts",
"../src/**/*.svelte",
"../test/**/*.js",
"../test/**/*.ts",
"../test/**/*.svelte",
"../tests/**/*.js",
"../tests/**/*.ts",
"../tests/**/*.svelte"
],
"exclude": [
"../node_modules/**",
"../src/service-worker.js",
"../src/service-worker/**/*.js",
"../src/service-worker.ts",
"../src/service-worker/**/*.ts",
"../src/service-worker.d.ts",
"../src/service-worker/**/*.d.ts"
]
}
@@ -0,0 +1,3 @@
{
"/": []
}
+23
View File
@@ -0,0 +1,23 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type MatcherParam<M> = M extends (param : string) => param is (infer U extends string) ? U : string;
type RouteParams = { };
type RouteId = '/';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | null
type LayoutParams = RouteParams & { }
type LayoutParentData = EnsureDefined<{}>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
export type LayoutServerData = null;
export type LayoutData = Expand<LayoutParentData>;
export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet }
+31
View File
@@ -0,0 +1,31 @@
{
"name": "famchamp",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^26.0.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.8.0",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"vite": "^8.0.16"
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+9
View File
@@ -0,0 +1,9 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}
+2
View File
@@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
+29
View File
@@ -0,0 +1,29 @@
import tailwindcss from '@tailwindcss/vite';
import adapter from '@sveltejs/adapter-node';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { PROXY_PORT } from '../config';
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit({
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true,
experimental: { async: true }
},
adapter: adapter(),
experimental: { remoteFunctions: true, handleRenderingErrors: true }
})
],
server: {
proxy: {
'/api': {
target: `http://localhost:${PROXY_PORT}`,
changeOrigin: true
}
}
}
});
+1358
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
packages:
- 'frontend'
- 'proxy'
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- esbuild