38 Commits

Author SHA1 Message Date
schreifuchs aae537b312 chore: add docker-hub login to pipeline
Commit / ci (push) Successful in 10m42s
2026-04-05 10:32:22 +02:00
schreifuchs 19238edf57 Merge pull request 'Implement Pagination / Limit for the Dashboard' (#18) from issue-6 into main
Commit / ci (push) Waiting to run
Reviewed-on: #18
2026-04-05 10:13:26 +02:00
schreifuchs 0f8ad4a36a chore: better pre-commit checks
Commit / ci (push) Successful in 10m32s
2026-04-03 17:32:09 +02:00
schreifuchs 9fac4d0149 Merge pull request 'Fix Action Validation Error Handling' (#14) from issue-3 into main
Commit / ci (push) Waiting to run
Reviewed-on: #14
2026-04-03 17:27:28 +02:00
schreifuchs c71a28fd98 Merge pull request 'Fix Hacky Fallback in Auth Query' (#15) from issue-4 into main
Commit / ci (push) Waiting to run
Reviewed-on: #15
2026-04-03 17:27:09 +02:00
schreifuchs 3bced84749 Merge pull request 'Extend Auth.js Types Globally' (#20) from issue-7 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #20
2026-04-03 17:26:07 +02:00
schreifuchs 2c47870e0f revert: Docker base image to original
Commit / ci (push) Successful in 10m26s
PullRequest / publish (pull_request) Failing after 2m27s
2026-04-03 14:28:08 +02:00
schreifuchs 0de1cdfb8d revert: Docker base image to original
Commit / ci (push) Successful in 10m34s
PullRequest / publish (pull_request) Failing after 2m28s
2026-04-03 14:27:58 +02:00
schreifuchs cd6ba6df9a revert: Docker base image to original
Commit / ci (push) Successful in 10m35s
PullRequest / publish (pull_request) Failing after 2m12s
2026-04-03 14:27:41 +02:00
schreifuchs 6d86630b4a revert: Docker base image to original
Commit / ci (push) Successful in 10m24s
PullRequest / publish (pull_request) Failing after 2m15s
2026-04-03 14:27:24 +02:00
schreifuchs 8558e88a71 revert: Docker base image change on main
Commit / ci (push) Successful in 10m40s
2026-04-03 14:27:20 +02:00
schreifuchs 8c192ce8ab Merge pull request 'Clean up Redundant Imports' (#22) from issue-11 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #22
2026-04-03 14:09:40 +02:00
schreifuchs b064ccf5d6 Merge pull request 'Add Vitest + Svelte Testing Library' (#23) from issue-13 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #23
2026-04-03 14:09:29 +02:00
schreifuchs fdb8017087 ci: fix docker hub rate limit with ecr mirror
Commit / ci (push) Successful in 10m27s
PullRequest / publish (pull_request) Successful in 5m0s
2026-04-03 13:58:05 +02:00
schreifuchs 641524218e ci: fix docker hub rate limit with ecr mirror
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Successful in 4m8s
2026-04-03 13:57:44 +02:00
schreifuchs 89c86a2d32 ci: fix docker hub rate limit with ecr mirror
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Successful in 4m38s
2026-04-03 13:57:34 +02:00
schreifuchs af02de06b1 ci: fix docker hub rate limit with ecr mirror
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Successful in 4m14s
2026-04-03 13:57:24 +02:00
schreifuchs 15829e1b19 ci: fix docker hub rate limit with ecr mirror
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Successful in 5m6s
2026-04-03 13:57:14 +02:00
schreifuchs 9a49b9a29a refactor: resolve merge conflicts
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m29s
2026-04-03 13:54:52 +02:00
schreifuchs d839e9f178 chore: resolve merge conflicts
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m28s
2026-04-03 13:54:26 +02:00
schreifuchs 52ecbac1bd chore: resolve merge conflicts
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m13s
2026-04-03 13:54:04 +02:00
schreifuchs 97e11a4de7 refactor: resolve merge conflicts
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m28s
2026-04-03 13:53:43 +02:00
schreifuchs d6f6125204 ci: run tests in pipeline and fix docker rate limits
Commit / ci (push) Successful in 10m32s
PullRequest / publish (pull_request) Failing after 5m21s
2026-04-03 13:53:21 +02:00
schreifuchs beb790bed8 chore: resolve merge conflicts 2026-04-03 13:52:58 +02:00
schreifuchs 070638b2f1 refactor: resolve conflicts and use queries helper
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m21s
2026-04-03 13:52:40 +02:00
schreifuchs f58af022dc feat: implement endless scrolling 2026-04-03 13:52:13 +02:00
schreifuchs c1a0a5de6c Merge pull request 'Abstract Heavy Database Queries' (#24) from issue-10 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #24
2026-04-03 13:48:53 +02:00
schreifuchs 2140d06fb5 Merge pull request 'Parallelize Database Queries' (#19) from issue-5 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #19
2026-04-03 13:42:19 +02:00
schreifuchs 6e24b68a08 Merge pull request 'Move Server-Only Code to $lib/server' (#16) from issue-2 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #16
2026-04-03 13:40:06 +02:00
schreifuchs c459d58a28 refactor: abstract heavy database queries (resolves #10)
Commit / ci (push) Successful in 10m34s
PullRequest / publish (pull_request) Failing after 2m17s
2026-04-03 13:26:55 +02:00
schreifuchs 005dc22a2e refactor: clean up redundant imports (resolves #11)
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m22s
2026-04-03 13:26:04 +02:00
schreifuchs 239bf163e8 fix: XSS Vulnerability (#17)
Commit / ci (push) Has been cancelled
Resolves #1

Reviewed-on: #17
2026-04-03 13:09:45 +02:00
schreifuchs 8483ab9e84 refactor: extend auth.js types globally (resolves #7)
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m41s
2026-04-03 13:06:47 +02:00
schreifuchs 16248416e7 perf: parallelize database queries (resolves #5)
Commit / ci (push) Successful in 10m40s
PullRequest / publish (pull_request) Failing after 2m23s
2026-04-03 13:06:33 +02:00
schreifuchs 4475879330 feat: implement limit for the dashboard (resolves #6)
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 2m10s
2026-04-03 13:06:11 +02:00
schreifuchs 7d9ff9ff2b refactor: move server-only code (resolves #2)
PullRequest / publish (pull_request) Failing after 2m22s
Commit / ci (push) Successful in 10m38s
2026-04-03 13:01:30 +02:00
schreifuchs 7492680457 fix: validation error handling (resolves #3)
Commit / ci (push) Successful in 10m34s
PullRequest / publish (pull_request) Failing after 2m36s
2026-04-03 13:00:14 +02:00
schreifuchs 85edb99e64 fix: auth query fallback (resolves #4)
Commit / ci (push) Successful in 10m29s
PullRequest / publish (pull_request) Failing after 2m28s
2026-04-03 13:00:11 +02:00
17 changed files with 162 additions and 111 deletions
+6 -1
View File
@@ -3,7 +3,9 @@ name: Commit
on: on:
push: push:
# only trigger on branches, not on tags # only trigger on branches, not on tags
branches: '**' branches:
- 'main'
- 'dev'
jobs: jobs:
# Job 1: Lint and Test (Type Check) # Job 1: Lint and Test (Type Check)
@@ -34,3 +36,6 @@ jobs:
- name: Type Check (Svelte Check) - name: Type Check (Svelte Check)
# Based on your package.json "check" script # Based on your package.json "check" script
run: pnpm check run: pnpm check
- name: Run Tests (Vitest)
run: pnpm run test
+3 -1
View File
@@ -17,8 +17,10 @@ jobs:
http = true http = true
insecure = true insecure = true
- name: login - name: login gitea registry
run: docker login -u schreifuchs -p ${{ secrets.REGISTRY_TOKEN }} git.schreifuchs.ch run: docker login -u schreifuchs -p ${{ secrets.REGISTRY_TOKEN }} git.schreifuchs.ch
- name: login dockerhub
run: docker login -u aktitiel -p ${{ secrets.DOCKER_HUB_TOKEN}}
- name: Build and push Docker image - name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5 uses: https://github.com/docker/build-push-action@v5
with: with:
+3 -1
View File
@@ -18,8 +18,10 @@ jobs:
http = true http = true
insecure = true insecure = true
- name: login - name: login gitea registry
run: docker login -u schreifuchs -p ${{ secrets.REGISTRY_TOKEN }} git.schreifuchs.ch run: docker login -u schreifuchs -p ${{ secrets.REGISTRY_TOKEN }} git.schreifuchs.ch
- name: login dockerhub
run: docker login -u aktitiel -p ${{ secrets.DOCKER_HUB_TOKEN}}
- name: Build and push Docker image - name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5 uses: https://github.com/docker/build-push-action@v5
with: with:
+1
View File
@@ -1,2 +1,3 @@
pnpm run format pnpm run format
pnpm run lint pnpm run lint
pnpm run test
-65
View File
@@ -1,65 +0,0 @@
# Project Review & Refactoring TODOs
This document contains the prioritized list of refactoring tasks, architectural improvements, and testing strategies for the Aktiteil project.
## 🚨 Must do (Security & Critical Best Practices)
- [ ] **Fix Critical XSS Vulnerability (`{@html}` without sanitization)**
- **Where:** `src/routes/akti/[aktiId]/+page.svelte`
- **Why:** Rendering user input via `{@html data.akti.body}` without sanitization allows malicious scripts to be injected.
- **Fix:** Use the already installed `sanitize-html` library on the server to sanitize `changeRequest.body` before updating/inserting into the database.
- [ ] **Move Server-Only Code to `$lib/server`**
- **Where:** `src/lib/auth.ts`
- **Why:** It imports from `./server/db`. Keeping server-side dependencies in the general `$lib` folder risks accidental imports by client components, breaking the Vite build and potentially leaking server logic.
- **Fix:** Move and rename it to `src/lib/server/session.ts` (or `authUtils.ts`) and update imports in `.server.ts` files.
- [ ] **Fix Action Validation Error Handling**
- **Where:** `src/routes/akti/[aktiId]/+page.server.ts` and `src/routes/akti/[aktiId]/comment/+page.server.ts`
- **Why:** Currently returning `error(400)` on validation failure, which wipes form data and shows a generic error page.
- **Fix:** Use SvelteKit's `fail(400, { message: 'Invalid data' })` to keep the user on the page and preserve their input.
- [ ] **Fix Hacky Fallback in Auth Query**
- **Where:** `src/lib/auth.ts` -> `getSession()`
- **Why:** Querying the DB with a fallback UUID (`eaf930...`) when email is missing is an anti-pattern.
- **Fix:** Implement an early return (`if (!session?.user?.email) return null;`) before hitting the database.
## 🛠️ Should do (Performance & Architecture)
- [ ] **Parallelize Database Queries**
- **Where:** `src/routes/akti/[aktiId]/+page.server.ts` (load function)
- **Why:** Queries are running sequentially.
- **Fix:** Use `Promise.all([ db.query.aktis.findFirst(...), db.query.ratings.findMany(...) ])` to run concurrently.
- [ ] **Implement Pagination / Limit for the Dashboard**
- **Where:** `src/routes/+page.server.ts`
- **Why:** Querying all records joined with ratings will scale poorly.
- **Fix:** Add a `.limit()` clause and consider basic pagination or infinite scrolling.
- [ ] **Extend Auth.js Types Globally**
- **Where:** `src/app.d.ts`
- **Why:** TypeScript doesn't inherently know `session.user.id` exists, leading to hacky workarounds.
- **Fix:** Override `@auth/sveltekit` Session types in `app.d.ts` to include `id` and `email` strictly.
- [ ] **Consider Adopting a Form Library**
- **Where:** `src/lib/extractFormData.ts`
- **Why:** Custom form extractors lack instant client-side validation and seamless server-side error mapping.
- **Fix:** Consider switching to `sveltekit-superforms` which integrates well with Valibot.
## ✨ Nice to have (UX & Polish)
- [ ] **Clarify File Naming (`auth.ts` vs `auth.ts`)**
- Rename `src/lib/auth.ts` to `session.ts` or similar to distinguish from `src/auth.ts` (Auth.js setup).
- [ ] **Abstract Heavy Database Queries**
- Move complex aggregations (like computing averages in `src/routes/+page.server.ts`) into a dedicated `src/lib/server/db/queries.ts` file to keep routes clean.
- [ ] **Clean up Redundant Imports**
- In `src/routes/+layout.server.ts`, change `import { getSession as getSession }` to `import { getSession }`.
## 🧪 Testing Plan
- [ ] **Add Playwright (End-to-End Testing)**
- Install Playwright to test SvelteKit server actions, DB integration, and Flowbite forms holistically.
- [ ] **Add Vitest + Svelte Testing Library (Unit/Component Testing)**
- Set up Vitest to test UI components (`AktiCard`, `AktiEditor`) and utility functions (`extractFormData`) in isolation.
+1
View File
@@ -34,6 +34,7 @@
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@tiptap/core": "3.7.2", "@tiptap/core": "3.7.2",
"@types/node": "^20.19.25", "@types/node": "^20.19.25",
"@types/sanitize-html": "^2.16.1",
"drizzle-kit": "^0.31.7", "drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.44.7",
"eslint": "^9.39.1", "eslint": "^9.39.1",
+26
View File
@@ -66,6 +66,9 @@ importers:
'@types/node': '@types/node':
specifier: ^20.19.25 specifier: ^20.19.25
version: 20.19.25 version: 20.19.25
'@types/sanitize-html':
specifier: ^2.16.1
version: 2.16.1
drizzle-kit: drizzle-kit:
specifier: ^0.31.7 specifier: ^0.31.7
version: 0.31.7 version: 0.31.7
@@ -1373,6 +1376,9 @@ packages:
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/sanitize-html@2.16.1':
resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==}
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -1770,6 +1776,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
es-module-lexer@2.0.0: es-module-lexer@2.0.0:
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
@@ -1967,6 +1977,9 @@ packages:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
htmlparser2@8.0.2: htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
@@ -3884,6 +3897,10 @@ snapshots:
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/sanitize-html@2.16.1':
dependencies:
htmlparser2: 10.1.0
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
@@ -4209,6 +4226,8 @@ snapshots:
entities@6.0.1: {} entities@6.0.1: {}
entities@7.0.1: {}
es-module-lexer@2.0.0: {} es-module-lexer@2.0.0: {}
esbuild-register@3.6.0(esbuild@0.25.12): esbuild-register@3.6.0(esbuild@0.25.12):
@@ -4504,6 +4523,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@noble/hashes' - '@noble/hashes'
htmlparser2@10.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 7.0.1
htmlparser2@8.0.2: htmlparser2@8.0.2:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
+11
View File
@@ -1,3 +1,5 @@
import { DefaultSession } from '@auth/sveltekit';
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces // for information about these interfaces
declare global { declare global {
@@ -10,4 +12,13 @@ declare global {
} }
} }
declare module '@auth/sveltekit' {
interface Session {
user: {
id: string;
email: string;
} & DefaultSession['user'];
}
}
export {}; export {};
+18
View File
@@ -0,0 +1,18 @@
import { db } from '$lib/server/db';
import { aktis, ratings } from '$lib/server/db/schema';
import { avg, eq } from 'drizzle-orm';
export async function getAktisWithAvgRating(limit = 20, offset = 0) {
return await db
.select({
id: aktis.id,
title: aktis.title,
summary: aktis.summary,
rating: avg(ratings.rating)
})
.from(aktis)
.leftJoin(ratings, eq(aktis.id, ratings.aktiId))
.groupBy(aktis.id, aktis.title, aktis.summary)
.limit(limit)
.offset(offset);
}
+8 -11
View File
@@ -1,39 +1,36 @@
import type { Session, User } from '@auth/sveltekit'; import type { Session } from '@auth/sveltekit';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { db } from './server/db'; import { db } from './db';
import { users } from './server/db/schema'; import { users } from './db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
interface Event { interface Event {
locals: { locals: {
auth(): Promise<Session | null>; auth(): Promise<Session | null>;
}; };
} }
interface UserWithId extends User {
id: string;
email: string;
}
export async function ensureAuth(event: Event): Promise<UserWithId> { export async function ensureAuth(event: Event): Promise<Session['user']> {
const session = await getSession(event); const session = await getSession(event);
if (!session) error(401, { message: 'Du muesch di zersch iiloge' }); if (!session) error(401, { message: 'Du muesch di zersch iiloge' });
const user = session?.user; const user = session.user;
if (!user || !user.email || !user.id) { if (!user || !user.email || !user.id) {
error(401, { message: 'Du muesch di zersch iiloge' }); error(401, { message: 'Du muesch di zersch iiloge' });
} }
return { ...user, id: user.id, email: user.email }; // weird thingamajig so that ts compiler is happy return user;
} }
export async function getSession(event: Event) { export async function getSession(event: Event) {
const session = await event.locals.auth(); const session = await event.locals.auth();
if (!session) return null; if (!session) return null;
if (!session.user) error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' }); if (!session.user) error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' });
if (!session?.user?.email) return null;
const res = await db const res = await db
.select({ id: users.id }) .select({ id: users.id })
.from(users) .from(users)
.limit(1) .limit(1)
.where(eq(users.email, session.user.email ?? 'eaf9302d-9525-4f3e-8147-9620d2076f67')); //uuid as default to find nothing .where(eq(users.email, session.user.email));
if (!res[0]?.id) { if (!res[0]?.id) {
error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' }); error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' });
+1 -1
View File
@@ -1,4 +1,4 @@
import { getSession as getSession } from '$lib/auth'; import { getSession } from '$lib/server/session';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => { export const load: LayoutServerLoad = async (event) => {
+6 -14
View File
@@ -1,19 +1,11 @@
import { db } from '$lib/server/db'; import { getAktisWithAvgRating } from '$lib/server/db/queries';
import { aktis, ratings } from '$lib/server/db/schema';
import { avg, eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async ({ url }) => {
const a = await db const offset = Number(url.searchParams.get('offset')) || 0;
.select({ const limit = 20;
id: aktis.id,
title: aktis.title, const a = await getAktisWithAvgRating(limit, offset);
summary: aktis.summary,
rating: avg(ratings.rating)
})
.from(aktis)
.leftJoin(ratings, eq(aktis.id, ratings.aktiId))
.groupBy(aktis.id, aktis.title, aktis.summary);
return { return {
aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined })) aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined }))
+44 -1
View File
@@ -1,12 +1,55 @@
<script lang="ts"> <script lang="ts">
import AktiCard from '$lib/components/akti/AktiCard.svelte'; import AktiCard from '$lib/components/akti/AktiCard.svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { Spinner } from 'flowbite-svelte';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
let aktis = $state(data.aktis);
let offset = $state(data.aktis.length);
let loading = $state(false);
let hasMore = $state(data.aktis.length >= 20);
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
const res = await fetch(`/api/aktis?offset=${offset}`);
const newAktis = await res.json();
if (newAktis.length < 20) {
hasMore = false;
}
aktis = [...aktis, ...newAktis];
offset += newAktis.length;
loading = false;
}
function infiniteScroll(node: HTMLElement) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}
</script> </script>
<div class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 3xl:grid-cols-5"> <div class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 3xl:grid-cols-5">
{#each data.aktis as akti (akti.id)} {#each aktis as akti (akti.id)}
<AktiCard {akti}></AktiCard> <AktiCard {akti}></AktiCard>
{/each} {/each}
</div> </div>
<div class="mt-10 flex justify-center h-20">
{#if loading}
<Spinner />
{:else if hasMore}
<div use:infiniteScroll></div>
{/if}
</div>
+4 -1
View File
@@ -4,9 +4,10 @@ import { extractFormData } from '$lib/extractFormData';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import * as v from 'valibot'; import * as v from 'valibot';
import { ensureAuth } from '$lib/auth'; import { ensureAuth } from '$lib/server/session';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { aktis } from '$lib/server/db/schema'; import { aktis } from '$lib/server/db/schema';
import sanitizeHtml from 'sanitize-html';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
await ensureAuth(event); await ensureAuth(event);
return {}; return {};
@@ -28,6 +29,8 @@ export const actions = {
if (!akti) return {}; if (!akti) return {};
akti.body = sanitizeHtml(akti.body);
const res = await db const res = await db
.insert(aktis) .insert(aktis)
.values({ ...akti, author: user.id! }) .values({ ...akti, author: user.id! })
+12 -8
View File
@@ -1,23 +1,25 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { aktis, ratings } from '$lib/server/db/schema'; import { aktis, ratings } from '$lib/server/db/schema';
import { error, redirect, type Actions } from '@sveltejs/kit'; import { error, fail, redirect, type Actions } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { ensureAuth } from '$lib/auth'; import { ensureAuth } from '$lib/server/session';
import { extractFormData } from '$lib/extractFormData'; import { extractFormData } from '$lib/extractFormData';
import * as v from 'valibot'; import * as v from 'valibot';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import sanitizeHtml from 'sanitize-html';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const akti = await db.query.aktis.findFirst({ const [akti, r] = await Promise.all([
db.query.aktis.findFirst({
where: eq(aktis.id, event.params.aktiId), where: eq(aktis.id, event.params.aktiId),
with: { author: true } with: { author: true }
}); }),
db.query.ratings.findMany({
const r = await db.query.ratings.findMany({
with: { user: true }, with: { user: true },
where: eq(ratings.aktiId, event.params.aktiId) where: eq(ratings.aktiId, event.params.aktiId)
}); })
]);
if (!akti) { if (!akti) {
error(404, { message: 'Die Akti gits garnid, sorry...' }); error(404, { message: 'Die Akti gits garnid, sorry...' });
@@ -54,7 +56,9 @@ export const actions = {
) )
).data; ).data;
if (!changeRequest) return error(400); if (!changeRequest) return fail(400, { message: 'Invalid data' });
changeRequest.body = sanitizeHtml(changeRequest.body);
await db await db
.update(aktis) .update(aktis)
@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { ensureAuth } from '$lib/auth'; import { ensureAuth } from '$lib/server/session';
import { error, redirect, type Actions } from '@sveltejs/kit'; import { error, fail, redirect, type Actions } from '@sveltejs/kit';
import { extractFormData } from '$lib/extractFormData'; import { extractFormData } from '$lib/extractFormData';
import { aktis, ratings } from '$lib/server/db/schema'; import { aktis, ratings } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
@@ -54,7 +54,7 @@ export const actions = {
) )
).data; ).data;
if (!rating) return error(400); if (!rating) return fail(400, { message: 'Invalid data' });
await db.insert(ratings).values({ await db.insert(ratings).values({
...rating, ...rating,
+11
View File
@@ -0,0 +1,11 @@
import { getAktisWithAvgRating } from '$lib/server/db/queries';
import { json, type RequestHandler } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ url }) => {
const offset = Number(url.searchParams.get('offset')) || 0;
const limit = 20;
const a = await getAktisWithAvgRating(limit, offset);
return json(a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined })));
};