Starting webapp dev.

This commit is contained in:
2026-05-08 22:14:01 -04:00
parent f62237dc3f
commit 42b48bee10
26 changed files with 6455 additions and 3 deletions
+49
View File
@@ -0,0 +1,49 @@
from fastapi import APIRouter, Header
from dataclasses import dataclass, field
from typing import List
from .auth import AuthRepo
from ..data.models import Basket, BasketDonorRel, Donor, Prefix, Ticket
from ..data.repos import BasketRepo, DonorRepo, PrefixRepo, TicketRepo
@dataclass
class BackupFile:
baskets: List[Basket] = field(default_factory=list)
donors: List[Donor] = field(default_factory=list)
donor_rels: List[BasketDonorRel] = field(default_factory=list)
prefixes: List[Prefix] = field(default_factory=list)
tickets: List[Ticket] = field(default_factory=list)
backup_router = APIRouter(prefix="/api/backup")
def chunk_list(in_list: list, chunk_size: int):
out_list = []
for i in range(0, len(in_list), chunk_size):
out_list.append([*in_list[i:i+chunk_size]])
return out_list
@backup_router.get("/backup.json")
def get_backup_file(tam_auth_key: str = Header("")):
auth_repo = AuthRepo()
auth_repo.verify_key(tam_auth_key)
del auth_repo
return BackupFile(
baskets=BasketRepo().get_all_baskets(),
donors=DonorRepo().get_all_donors(),
donor_rels=DonorRepo().get_all_donor_relations(),
prefixes=PrefixRepo().get_prefixes(),
tickets=TicketRepo().get_all_tickets()
)
@backup_router.post("/restore")
def post_backup_file(backup_file: BackupFile, tam_auth_key: str = Header("")):
for bs in chunk_list(backup_file.baskets, 300):
BasketRepo().post_baskets(bs)
for ds in chunk_list(backup_file.donors, 300):
DonorRepo().post_donors(ds)
for drs in chunk_list(backup_file.donor_rels, 300):
DonorRepo().post_donor_relation(drs)
for ps in chunk_list(backup_file.prefixes, 300):
PrefixRepo().post_prefixes(ps)
for ts in chunk_list(backup_file.tickets, 300):
TicketRepo().post_tickets(ts)
return {"detail": "File uploaded successfully."}
+3 -3
View File
@@ -39,9 +39,9 @@ def init_db():
donor_business TEXT
)""")
cur.execute("""CREATE TABLE IF NOT EXISTS r_basket_donor (
b_prefix TEXT REFERENCES baskets(prefix),
b_id INT REFERENCES baskets(basket_id),
d_id INT REFERENCES donors(donor_id),
b_prefix TEXT,
b_id INT,
d_id INT,
PRIMARY KEY (b_prefix, b_id, d_id)
)""")
cur.execute("""CREATE VIEW IF NOT EXISTS winners_by_basket AS
+16
View File
@@ -29,6 +29,16 @@ class PrefixRepo(RepoTemplate):
self.conn.commit()
return {"detail": "Prefix posted successfully."}
def post_prefixes(self, ps: list[Prefix]):
"""Posts a list of prefixes."""
for p in ps:
self.cur.execute(
"INSERT INTO prefixes VALUES (?, ?, ?) ON CONFLICT (prefix) DO UPDATE SET color = EXCLUDED.color, weight = EXCLUDED.weight",
(p.prefix, p.color, p.weight),
)
self.conn.commit()
return {"detail": "Prefixes posted successfully."}
def del_prefix(self, prefix: str):
"""Deletes a prefix from the database."""
self.cur.execute("DELETE FROM prefixes WHERE prefix = ?", (prefix,))
@@ -172,6 +182,12 @@ class DonorRepo(RepoTemplate):
results = self.cur.fetchall()
return [BasketDonorView(*r) for r in results]
def get_all_donor_relations(self):
"""Gets all donor relations, typically for backup purposes."""
self.cur.execute("SELECT * FROM r_basket_donor")
results = self.cur.fetchall()
return [BasketDonorRel(*r) for r in results]
def post_donors(self, ds: list[Donor]):
"""Posts donors"""
rtn_lst = []
+2
View File
@@ -1,6 +1,7 @@
from fastapi import FastAPI
from .core.auth import auth_router
from .core.backuprestore import backup_router
from .data import routers
from .reports import routers as report_routers
from .search import routers as search_routers
@@ -15,3 +16,4 @@ def append_routers(app: FastAPI):
app.include_router(routers.donor_router)
app.include_router(report_routers.reports_router)
app.include_router(search_routers.search_router)
app.include_router(backup_router)
+2
View File
@@ -0,0 +1,2 @@
# Drizzle
DATABASE_URL=local.db
+25
View File
@@ -0,0 +1,25 @@
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-*
# SQLite
*.db
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint"
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}
+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
npx sv@0.15.2 create --template minimal --no-types --add sveltekit-adapter="adapter:node" tailwindcss="plugins:none" drizzle="database:sqlite+sqlite:better-sqlite3" eslint --install npm ./webapp
```
## 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.
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit';
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
export default defineConfig({
schema: './src/lib/server/db/schema.js',
dialect: 'sqlite',
dbCredentials: { url: process.env.DATABASE_URL },
verbose: true,
strict: true
});
+29
View File
@@ -0,0 +1,29 @@
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig([
includeIgnoreFile(gitignorePath),
js.configs.recommended,
svelte.configs.recommended,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } }
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: { parserOptions: { svelteConfig } }
},
{
// Override or add rule settings here, such as:
// 'svelte/button-has-type': 'error'
rules: {}
}
]);
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"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
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+6064
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "webapp",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"lint": "eslint .",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
"@eslint/compat": "^2.0.4",
"@eslint/js": "^10.0.1",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^4.2.2",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^24",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
"eslint": "^10.2.0",
"eslint-plugin-svelte": "^3.17.0",
"globals": "^17.4.0",
"svelte": "^5.55.2",
"tailwindcss": "^4.2.2",
"vite": "^8.0.7"
},
"dependencies": {
"better-sqlite3": "^12.8.0"
}
}
+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.
+10
View File
@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
import { env } from '$env/dynamic/private';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = new Database(env.DATABASE_URL);
export const db = drizzle(client, { schema });
+84
View File
@@ -0,0 +1,84 @@
import { integer, sqliteTable, sqliteView, text, primaryKey } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const prefixes = sqliteTable('prefixes', {
prefix: text('prefix').primaryKey(),
color: text('color'),
weight: integer('weight')
});
export const tickets = sqliteTable('tickets', {
prefix: text('prefix'),
ticket_id: integer('ticket_id'),
first_name: text('first_name'),
last_name: text('last_name'),
phone_number: text('phone_number'),
pref: text('pref')
}, (t) => [primaryKey(t.prefix, t.ticket_id)])
export const baskets = sqliteTable('baskets', {
prefix: text('prefix'),
basket_id: integer('basket_id'),
description: text('description'),
winning_ticket: integer('winning_ticket')
}, (b) => [primaryKey(b.prefix, b.basket_id)])
export const donors = sqliteTable('donors', {
donor_id: integer('donor_id').primaryKey({ autoIncrement: true }),
donor_name: text('donor_name'),
donor_business: text('donor_business')
})
export const r_basket_donor = sqliteTable('r_basket_donor', {
b_prefix: text('b_prefix'),
b_id: integer('b_id'),
d_id: integer('d_id')
}, (r) => [primaryKey(r.b_prefix, r.b_id, r.d_id)])
export const winners_by_basket = sqliteView('winners_by_basket', {
prefix: text('prefix'),
basket_id: integer('basket_id'),
description: text('description'),
winning_ticket: integer('winning_ticket'),
last_name: text('last_name'),
first_name: text('first_name'),
phone_number: text('phone_number'),
pref: text('pref')
}).as(sql`SELECT b.prefix, b.basket_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref
FROM baskets b LEFT JOIN tickets ON b.prefix = t.prefix AND b.winning_ticket = t.ticket_id
ORDER BY b.prefix, b.basket_id`)
export const winners_by_name = sqliteView('winners_by_name', {
prefix: text('prefix'),
last_name: text('last_name'),
first_name: text('first_name'),
phone_number: text('phone_number'),
basket_id: integer('basket_id'),
description: text('description'),
pref: text('pref')
}).as(sql`SELECT b.prefix, t.last_name, t.first_name, t.phone_number, b.basket_id, b.description, t.pref
FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.ticket_id
ORDER BY b.prefix, t.last_name, t.first_name, t.phone_number, b.basket_id`)
export const counts = sqliteView('counts', {
prefix: text('prefix'),
unique_buyers: integer('unique_buyers'),
total_buys: integer('total_buys')
}).as(sql`SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys
FROM tickets
GROUP BY prefix
UNION ALL
SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*)
FROM tickets`)
export const v_donors = sqliteView('v_donors', {
b_prefix: text('b_prefix'),
b_id: integer('b_id'),
d_id: integer('d_id'),
donor_name: text('donor_name'),
donor_business: text('donor_business'),
description: text('description')
}).as(sql`SELECT bd.b_prefix, bd.b_id, bd.d_id, d.donor_name, d.donor_business, b.description
FROM r_basket_donor bd LEFT JOIN donors d ON bd.d_id = d.donor_id
LEFT JOIN baskets b ON bd.b_prefix = b.prefix AND bd.b_id = b.basket_id
ORDER BY bd.b_prefix, bd.b_id`)
+9
View File
@@ -0,0 +1,9 @@
<script>
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 @@
import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
const config = {
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
},
kit: {
adapter: adapter(),
typescript: {
config: (config) => ({
...config,
include: [...config.include, '../drizzle.config.js']
})
}
}
};
export default config;
+5
View File
@@ -0,0 +1,5 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });