diff --git a/package.json b/package.json index 76728b8..1c5e453 100644 --- a/package.json +++ b/package.json @@ -1,79 +1,77 @@ { - "name": "km-challenge", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "bunx --bun vite dev", - "build": "bunx --bun vite build", - "preview": "bunx --bun vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", - "lint": "prettier --check . && eslint .", - "test:e2e": "playwright test", - "test": "npm run test:e2e", - "push": "drizzle-kit push", - "generate": "drizzle-kit generate", - "studio": "drizzle-kit studio", - "start": "PORT=8080 bun /build" - }, - "devDependencies": { - "@eslint/compat": "^1.2.3", - "@eslint/js": "^9.17.0", - "@internationalized/date": "^3.6.0", - "@playwright/test": "^1.45.3", - "@sveltejs/adapter-vercel": "^5.5.0", - "@sveltejs/kit": "^2.52.0", - "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@tailwindcss/vite": "^4.1.7", - "@types/pg": "^8.15.6", - "bits-ui": "1.5.2", - "clsx": "^2.1.1", - "daisyui": "^4.12.23", - "drizzle-kit": "^1.0.0-beta.6-4414a19", - "eslint": "^9.7.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "formsnap": "^2.0.1", - "globals": "^15.0.0", - "layerchart": "2.0.0-next.43", - "mode-watcher": "^0.5.0", - "prettier": "^3.3.2", - "prettier-plugin-svelte": "^3.2.6", - "prettier-plugin-tailwindcss": "^0.6.11", - "svelte": "^5.51.2", - "svelte-adapter-bun": "^1.0.1", - "svelte-check": "^4.4.0", - "svelte-sonner": "^1.0.1", - "sveltekit-superforms": "^2.29.1", - "tailwind-merge": "^3.3.0", - "tailwind-variants": "^1.0.0", - "tailwindcss": "^4.1.7", - "tw-animate-css": "^1.3.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0", - "vite": "^7.3.1", - "vite-plugin-devtools-json": "^1.0.0" - }, - "dependencies": { - "@better-fetch/fetch": "^1.1.12", - "@lucide/svelte": "^0.564.0", - "@nevthereal/random-utils": "^1.0.11", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "@sveltejs/adapter-node": "^5.4.0", - "@tailwindcss/typography": "^0.5.16", - "@tanstack/table-core": "^8.20.5", - "@upstash/redis": "^1.34.8", - "arctic": "^3.1.2", - "better-auth": "^1.4.9", - "dayjs": "^1.11.19", - "dotenv": "^16.4.7", - "drizzle-orm": "^1.0.0-beta.6-4414a19", - "drizzle-zod": "^0.6.1", - "pg": "^8.16.3", - "zod": "^4.2.1" - }, - "packageManager": "bun@1.3.5" + "name": "km-challenge", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "bunx --bun vite dev", + "build": "bunx --bun vite build", + "preview": "bunx --bun vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "test:e2e": "playwright test", + "test": "npm run test:e2e", + "push": "drizzle-kit push", + "generate": "drizzle-kit generate", + "studio": "drizzle-kit studio", + "start": "PORT=8080 bun /build" + }, + "devDependencies": { + "@eslint/compat": "^1.2.3", + "@eslint/js": "^9.17.0", + "@internationalized/date": "^3.6.0", + "@playwright/test": "^1.45.3", + "@sveltejs/adapter-vercel": "^5.5.0", + "@sveltejs/kit": "^2.52.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.1.7", + "@types/pg": "^8.15.6", + "bits-ui": "1.5.2", + "clsx": "^2.1.1", + "daisyui": "^4.12.23", + "drizzle-kit": "^1.0.0-beta.6-4414a19", + "eslint": "^9.7.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "layerchart": "2.0.0-next.43", + "mode-watcher": "^0.5.0", + "prettier": "^3.3.2", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.51.2", + "svelte-adapter-bun": "^1.0.1", + "svelte-check": "^4.4.0", + "svelte-sonner": "^1.0.1", + "tailwind-merge": "^3.3.0", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.7", + "tw-animate-css": "^1.3.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0", + "vite": "^7.3.1", + "vite-plugin-devtools-json": "^1.0.0" + }, + "dependencies": { + "@better-fetch/fetch": "^1.1.12", + "@lucide/svelte": "^0.564.0", + "@nevthereal/random-utils": "^1.0.11", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "@sveltejs/adapter-node": "^5.4.0", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/table-core": "^8.20.5", + "@upstash/redis": "^1.34.8", + "arctic": "^3.1.2", + "better-auth": "^1.4.9", + "dayjs": "^1.11.19", + "dotenv": "^16.4.7", + "drizzle-orm": "^1.0.0-beta.6-4414a19", + "drizzle-zod": "^0.6.1", + "pg": "^8.16.3", + "zod": "^4.2.1" + }, + "packageManager": "bun@1.3.5" } diff --git a/src/lib/components/DisciplineForm.svelte b/src/lib/components/DisciplineForm.svelte index 08cbe3a..077dfc2 100644 --- a/src/lib/components/DisciplineForm.svelte +++ b/src/lib/components/DisciplineForm.svelte @@ -1,24 +1,21 @@ @@ -28,58 +25,49 @@ Diszipline hinzufügen -
- - {#each $form.discipline as _, i} -
-
- - {#snippet children({ props })} -
- Name - -
- {/snippet} -
- - - {#snippet children({ props })} -
- Multiplikator - -
- {/snippet} -
- -
- -
- {/each} - -
- Speichern + { + await submit(); + sheetOpen = false; + rows = [{ name: '', multiplier: 1 }]; + })} + > + + {#each rows as row, i} +
+ + Name + + + + Multiplikator + + + +
+ {/each} + + +
diff --git a/src/lib/components/EntryCard.svelte b/src/lib/components/EntryCard.svelte index a775016..96b2a9d 100644 --- a/src/lib/components/EntryCard.svelte +++ b/src/lib/components/EntryCard.svelte @@ -1,10 +1,10 @@ @@ -37,8 +38,6 @@ -
- {#snippet deleteDialog()} @@ -54,10 +53,10 @@ Abbrechen (open = false)} + onclick={async () => { + await deleteEntry({ challengeId, entryId: entry.id }); + open = false; + }} class={buttonVariants({ variant: 'destructive' })}>Löschen diff --git a/src/lib/components/EntryForm.svelte b/src/lib/components/EntryForm.svelte index 557a466..5836073 100644 --- a/src/lib/components/EntryForm.svelte +++ b/src/lib/components/EntryForm.svelte @@ -1,83 +1,45 @@ @@ -94,95 +56,83 @@ Neuer Eintrag -
{ + await submit(); + dialogOpen = false; + })} class="text-left" > +

Bitte Kilometer roh eintragen, die Punkte werden später verrechnet

- - - {#snippet children({ props })} - Kilometer - - {/snippet} - - - - - - {#snippet children({ props })} - Disziplin - - - {$form.disciplineId ? getDiscipline($form.disciplineId) : 'Disziplin Wählen'} - - - {#each disciplines as discipline} - - {/each} - - - {/snippet} - - - + + Kilometer + + + + + + Disziplin + + + {selectedDiscipline + ? getDiscipline(selectedDiscipline) + : 'Disziplin Wählen'} + + + {#each disciplines as currentDiscipline} + + {/each} + + + +
- - - {#snippet children({ props })} - Datum der Aktivität - - - {value ? df.format(value.toDate(getLocalTimeZone())) : 'Datum wählen'} - - - - { - if (v) { - $form.date = v.toString(); - } else { - $form.date = ''; - } - }} - /> - - - - - {/snippet} - - + + Datum der Aktivität + + + {value ? df.format(value.toDate(getLocalTimeZone())) : 'Datum wählen'} + + + + { + if (v) { + entryForm.fields.date.set(v.toString()); + value = v; + } + }} + /> + + + + +
diff --git a/src/lib/components/ui/field/field-description.svelte b/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..2797451 --- /dev/null +++ b/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,15 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/field/field-error.svelte b/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 0000000..145422c --- /dev/null +++ b/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,19 @@ + + +{#if issues.length > 0} +
    + {#each issues as issue} +
  • {issue.message}
  • + {/each} +
+{/if} diff --git a/src/lib/components/ui/field/field-label.svelte b/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..5f13a19 --- /dev/null +++ b/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/ui/field/field.svelte b/src/lib/components/ui/field/field.svelte new file mode 100644 index 0000000..74a9dc5 --- /dev/null +++ b/src/lib/components/ui/field/field.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/field/index.ts b/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..57d88a6 --- /dev/null +++ b/src/lib/components/ui/field/index.ts @@ -0,0 +1,6 @@ +import Field from './field.svelte'; +import FieldDescription from './field-description.svelte'; +import FieldError from './field-error.svelte'; +import FieldLabel from './field-label.svelte'; + +export { Field, FieldDescription, FieldError, FieldLabel }; diff --git a/src/lib/components/ui/form/form-button.svelte b/src/lib/components/ui/form/form-button.svelte deleted file mode 100644 index cc0c590..0000000 --- a/src/lib/components/ui/form/form-button.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/form/form-description.svelte b/src/lib/components/ui/form/form-description.svelte deleted file mode 100644 index 6c70187..0000000 --- a/src/lib/components/ui/form/form-description.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/form/form-element-field.svelte b/src/lib/components/ui/form/form-element-field.svelte deleted file mode 100644 index 278395c..0000000 --- a/src/lib/components/ui/form/form-element-field.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - {#snippet children({ constraints, errors, tainted, value })} -
- {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} -
- {/snippet} -
diff --git a/src/lib/components/ui/form/form-field-errors.svelte b/src/lib/components/ui/form/form-field-errors.svelte deleted file mode 100644 index c866b83..0000000 --- a/src/lib/components/ui/form/form-field-errors.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - {#snippet children({ errors, errorProps })} - {#if childrenProp} - {@render childrenProp({ errors, errorProps })} - {:else} - {#each errors as error} -
{error}
- {/each} - {/if} - {/snippet} -
diff --git a/src/lib/components/ui/form/form-field.svelte b/src/lib/components/ui/form/form-field.svelte deleted file mode 100644 index 6f7a766..0000000 --- a/src/lib/components/ui/form/form-field.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - {#snippet children({ constraints, errors, tainted, value })} -
- {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} -
- {/snippet} -
diff --git a/src/lib/components/ui/form/form-fieldset.svelte b/src/lib/components/ui/form/form-fieldset.svelte deleted file mode 100644 index bc40a2a..0000000 --- a/src/lib/components/ui/form/form-fieldset.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/src/lib/components/ui/form/form-label.svelte b/src/lib/components/ui/form/form-label.svelte deleted file mode 100644 index b651454..0000000 --- a/src/lib/components/ui/form/form-label.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - {#snippet child({ props })} - - {/snippet} - diff --git a/src/lib/components/ui/form/form-legend.svelte b/src/lib/components/ui/form/form-legend.svelte deleted file mode 100644 index b0d76ad..0000000 --- a/src/lib/components/ui/form/form-legend.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/form/index.ts b/src/lib/components/ui/form/index.ts deleted file mode 100644 index 0713927..0000000 --- a/src/lib/components/ui/form/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as FormPrimitive from "formsnap"; -import Description from "./form-description.svelte"; -import Label from "./form-label.svelte"; -import FieldErrors from "./form-field-errors.svelte"; -import Field from "./form-field.svelte"; -import Fieldset from "./form-fieldset.svelte"; -import Legend from "./form-legend.svelte"; -import ElementField from "./form-element-field.svelte"; -import Button from "./form-button.svelte"; - -const Control = FormPrimitive.Control; - -export { - Field, - Control, - Label, - Button, - FieldErrors, - Description, - Fieldset, - Legend, - ElementField, - // - Field as FormField, - Control as FormControl, - Description as FormDescription, - Label as FormLabel, - FieldErrors as FormFieldErrors, - Fieldset as FormFieldset, - Legend as FormLegend, - ElementField as FormElementField, - Button as FormButton, -}; diff --git a/src/lib/remote/auth.remote.ts b/src/lib/remote/auth.remote.ts new file mode 100644 index 0000000..67c143e --- /dev/null +++ b/src/lib/remote/auth.remote.ts @@ -0,0 +1,18 @@ +import { getRequestEvent, query } from '$app/server'; +import { auth } from '$lib/auth'; +import { getUser } from '$lib/utils'; + +export function requireUser() { + const { locals, url } = getRequestEvent(); + return getUser({ locals, redirectUrl: url.pathname }); +} + +export const getCurrentUser = query(async () => { + return { user: requireUser() }; +}); + +export const getLayoutSession = query(async () => { + const { request } = getRequestEvent(); + const session = await auth.api.getSession({ headers: request.headers }); + return { session }; +}); diff --git a/src/lib/remote/challenge.remote.ts b/src/lib/remote/challenge.remote.ts new file mode 100644 index 0000000..0a1be80 --- /dev/null +++ b/src/lib/remote/challenge.remote.ts @@ -0,0 +1,516 @@ +import { command, form, getRequestEvent, query } from '$app/server'; +import { checkAdmin, db, getLeaderBoard } from '$lib/db'; +import { challenge, challengeMember, discipline, entry, clubAdmin as clubAdminTable } from '$lib/db/schema'; +import { requireUser } from '$lib/remote/auth.remote'; +import { canAddEntries, getDaysRemainingForEntry, prettyDate } from '$lib/utils'; +import { error, redirect } from '@sveltejs/kit'; +import { eq, and, getColumns, gte, lte } from 'drizzle-orm'; +import { z } from 'zod'; + +const challengeInput = z.object({ challengeId: z.string() }); +const clubChallengeInput = z.object({ clubId: z.string(), challengeId: z.string() }); + +const addEntrySchema = z.object({ + challengeId: z.string(), + disciplineId: z.string(), + amount: z.number().min(0.01), + date: z.string() +}); + +const editChallengeSchema = z + .object({ + clubId: z.string(), + challengeId: z.string(), + name: z.string().min(3), + startsAt: z.string(), + endsAt: z.string() + }) + .refine((data) => new Date(data.endsAt).getTime() > new Date(data.startsAt).getTime(), { + message: 'Enddatum muss nach dem Startdatum liegen', + path: ['endsAt'] + }); + +const addDisciplinesSchema = z.object({ + challengeId: z.string(), + discipline: z + .object({ + name: z.string().min(1), + multiplier: z.number().min(0.1) + }) + .array() +}); + +const deleteDisciplineSchema = z.object({ + challengeId: z.string(), + disciplineId: z.string() +}); + +const deleteEntrySchema = z.object({ + challengeId: z.string(), + entryId: z.string() +}); + +const challengeRouteSchema = z.object({ + clubId: z.string(), + challengeId: z.string() +}); + +const challengeMemberRouteSchema = z.object({ + clubId: z.string(), + challengeId: z.string(), + memberId: z.string() +}); + +async function getChallengeContext(clubId: string, challengeId: string) { + const user = requireUser(); + + if (!user.completedProfile) redirect(302, '/profile/edit'); + + const qChallenge = await db.query.challenge.findFirst({ + where: { id: challengeId }, + with: { + members: true, + entries: { + with: { + user: true, + discipline: true + } + }, + disciplines: true + } + }); + + if (!qChallenge) error(404, 'Challenge nicht gefunden'); + + const currentUserChallenge = await db.query.challengeMember.findFirst({ + where: { challengeId, userId: user.id } + }); + + const currentUserClub = await db.query.clubMember.findFirst({ + where: { clubId: qChallenge.clubId, userId: user.id } + }); + + if (!currentUserClub) redirect(302, '/clubs'); + + const clubAdmin = await checkAdmin(clubId, user.id); + const challengePath = `/clubs/${clubId}/challenge/${challengeId}`; + + return { + challenge: qChallenge, + user, + currentUserChallenge, + clubAdmin, + challengePath + }; +} + +export const getHomePageData = query(async () => { + const { locals } = getRequestEvent(); + const user = locals.user; + + if (!user) { + return { + user: null, + challengesWithLeaderboards: [], + openForEntriesChallenges: [] + }; + } + + const now = new Date(); + const startOfToday = new Date(now); + startOfToday.setHours(0, 0, 0, 0); + + const endOfToday = new Date(now); + endOfToday.setHours(23, 59, 59, 999); + + const activeChallenges = await db + .select({ + ...getColumns(challenge) + }) + .from(challengeMember) + .innerJoin(challenge, eq(challengeMember.challengeId, challenge.id)) + .where( + and( + eq(challengeMember.userId, user.id), + lte(challenge.startsAt, endOfToday), + gte(challenge.endsAt, startOfToday) + ) + ) + .groupBy(challenge.id); + + const challengesWithLeaderboards = await Promise.all( + activeChallenges.map(async (c) => { + const disciplines = await db.query.discipline.findMany({ where: { challengeId: c.id } }); + const leaderboard = getLeaderBoard.execute({ challengeId: c.id, limit: 5 }); + return { ...c, leaderboard, disciplines }; + }) + ); + + const openForEntriesChallenges = await db + .select({ + ...getColumns(challenge) + }) + .from(challengeMember) + .innerJoin(challenge, eq(challengeMember.challengeId, challenge.id)) + .where( + and( + eq(challengeMember.userId, user.id), + lte(challenge.endsAt, endOfToday), + gte(challenge.endsAt, new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)) + ) + ) + .groupBy(challenge.id); + + const openForEntriesChallengesWithDays = openForEntriesChallenges + .filter((c) => canAddEntries(c)) + .map((c) => ({ + ...c, + daysRemaining: getDaysRemainingForEntry(c) + })); + + return { + challengesWithLeaderboards, + user, + openForEntriesChallenges: openForEntriesChallengesWithDays + }; +}); + +export const getChallengeLayoutData = query(challengeRouteSchema, async ({ clubId, challengeId }) => { + return getChallengeContext(clubId, challengeId); +}); + +export const getChallengeOverviewData = query(challengeRouteSchema, async ({ clubId, challengeId }) => { + const context = await getChallengeContext(clubId, challengeId); + const { challenge } = context; + + const leaderboard = getLeaderBoard.execute({ + challengeId: challenge.id, + limit: challenge.members.length + }); + + const lastActivities = await db.query.entry.findMany({ + where: { + challengeId: challenge.id + }, + limit: 10, + orderBy(fields, operators) { + return operators.desc(fields.createdAt); + }, + with: { + user: true, + discipline: true + } + }); + + type MemberStats = { + userId: string; + name: string; + totalKm: number; + activityCount: number; + activeDays: Set; + }; + + const statsByUser = new Map(); + for (const challengeEntry of challenge.entries ?? []) { + const userId = challengeEntry.userId; + if (!statsByUser.has(userId)) { + statsByUser.set(userId, { + userId, + name: challengeEntry.user?.name ?? 'Unbekannt', + totalKm: 0, + activityCount: 0, + activeDays: new Set() + }); + } + + const stats = statsByUser.get(userId); + if (!stats) continue; + + stats.totalKm += Number(challengeEntry.amount); + stats.activityCount += 1; + stats.activeDays.add(new Date(challengeEntry.date).toISOString().slice(0, 10)); + } + + const rankedMembers = Array.from(statsByUser.values()); + const getPodium = (items: MemberStats[]) => ({ + winner: items[0]?.name ?? 'Offen', + runnerUp: items[1]?.name ?? 'Offen' + }); + + const consistencyRanking = [...rankedMembers].sort((a, b) => { + return ( + b.activeDays.size - a.activeDays.size || + b.activityCount - a.activityCount || + b.totalKm - a.totalKm + ); + }); + + const kmRanking = [...rankedMembers].sort((a, b) => { + return ( + b.totalKm - a.totalKm || + b.activityCount - a.activityCount || + b.activeDays.size - a.activeDays.size + ); + }); + + const activityRanking = [...rankedMembers].sort((a, b) => { + return ( + b.activityCount - a.activityCount || + b.activeDays.size - a.activeDays.size || + b.totalKm - a.totalKm + ); + }); + + const awards = [ + { + title: 'Consistency-King/Queen', + subtitle: 'An den meisten Tagen aktiv', + ...getPodium(consistencyRanking) + }, + { + title: 'KM-Master', + subtitle: 'Sammelt die meisten Kilometer', + ...getPodium(kmRanking) + }, + { + title: 'Aktivitäts-Champion', + subtitle: 'Meiste Aktivitäten insgesamt', + ...getPodium(activityRanking) + } + ]; + + const daysRemainingForEntry = canAddEntries(challenge) ? getDaysRemainingForEntry(challenge) : 0; + + return { ...context, leaderboard, lastActivities, daysRemainingForEntry, awards }; +}); + +export const getChallengeActivityData = query(challengeRouteSchema, async ({ clubId, challengeId }) => { + const context = await getChallengeContext(clubId, challengeId); + const { challenge, user } = context; + + const entries = await db.query.entry.findMany({ + where: { + userId: user.id, + challengeId: challenge.id + }, + with: { + discipline: true + }, + orderBy: { date: 'desc' } + }); + + const membership = await db.query.challengeMember.findFirst({ + where: { + userId: user.id, + challengeId: challenge.id + }, + with: { + user: true + } + }); + + if (!membership) error(400); + + return { ...context, entries, membership }; +}); + +export const getChallengeMembersData = query(challengeRouteSchema, async ({ clubId, challengeId }) => { + const context = await getChallengeContext(clubId, challengeId); + const { challenge } = context; + + const members = await db.query.challengeMember.findMany({ + where: { challengeId: challenge.id }, + with: { + user: { + columns: { + name: true, + gender: true, + role: true, + id: true, + admin: true + } + } + }, + orderBy(fields, operators) { + return operators.asc(fields.joinedAt); + } + }); + + const admins = await db.query.clubAdmin.findMany({ where: { clubId } }); + + return { ...context, members, admins }; +}); + +export const getChallengeMemberDetailsData = query( + challengeMemberRouteSchema, + async ({ clubId, challengeId, memberId }) => { + const context = await getChallengeContext(clubId, challengeId); + + const qMember = await db.query.challengeMember.findFirst({ + where: { AND: [{ userId: memberId }, { challengeId }] }, + with: { + user: { + with: { + entries: { + where: { + challengeId + }, + with: { + discipline: true + }, + orderBy: (fields, operators) => operators.desc(fields.date) + } + } + } + } + }); + + if (!qMember) error(404, 'Dieses Mitglied existiert nicht'); + + return { ...context, member: qMember }; + } +); + +export const getLeaderboard = query( + z.object({ challengeId: z.string(), limit: z.number().optional() }), + async ({ challengeId, limit = 10 }) => { + return getLeaderBoard.execute({ challengeId, limit }); + } +); + +export const addEntry = form(addEntrySchema, async ({ challengeId, disciplineId, amount, date }) => { + const user = requireUser(); + + const qDiscipline = await db.query.discipline.findFirst({ where: { id: disciplineId } }); + if (!qDiscipline) error(404, 'Disziplin nicht gefunden'); + + const qChallenge = await db.query.challenge.findFirst({ where: { id: qDiscipline.challengeId } }); + if (!qChallenge) error(404, 'Challenge nicht gefunden'); + + if (!canAddEntries(qChallenge)) error(403, 'Diese Challenge akzeptiert keine Einträge mehr'); + + const entryDate = new Date(date); + const challengeStart = new Date(qChallenge.startsAt); + challengeStart.setUTCHours(0, 0, 0, 0); + const challengeEnd = new Date(qChallenge.endsAt); + challengeEnd.setUTCHours(23, 59, 59, 999); + + if (entryDate < challengeStart || entryDate > challengeEnd) { + error( + 400, + `Die Aktivität muss zwischen ${prettyDate(qChallenge.startsAt)} und ${prettyDate(qChallenge.endsAt)} stattgefunden haben` + ); + } + + await db.insert(entry).values({ + amount: amount.toString(), + challengeId, + date: new Date(date), + disciplineId, + userId: user.id + }); +}); + +export const deleteChallenge = command(clubChallengeInput, async ({ clubId, challengeId }) => { + const user = requireUser(); + const qChallenge = await db.query.challenge.findFirst({ where: { id: challengeId } }); + if (!qChallenge) error(404, 'Challenge existiert nicht'); + + const isAdmin = await checkAdmin(clubId, user.id); + if (!isAdmin) error(401, 'Nicht erlaubt'); + + await db.delete(challenge).where(eq(challenge.id, qChallenge.id)); + redirect(302, `/clubs/${qChallenge.clubId}`); +}); + +export const editChallenge = form(editChallengeSchema, async ({ clubId, challengeId, name, endsAt, startsAt }) => { + const user = requireUser(); + const qChallenge = await db.query.challenge.findFirst({ where: { id: challengeId } }); + if (!qChallenge) error(404, 'Challenge existiert nicht'); + + const isAdmin = await checkAdmin(clubId, user.id); + if (!isAdmin) error(401, 'Nicht erlaubt'); + + await db + .update(challenge) + .set({ + name, + endsAt: new Date(endsAt), + startsAt: new Date(startsAt) + }) + .where(eq(challenge.id, qChallenge.id)); +}); + +export const leaveChallenge = command(challengeInput, async ({ challengeId }) => { + const user = requireUser(); + + const qChallengeMember = await db.query.challengeMember.findFirst({ + where: { AND: [{ challengeId }, { userId: user.id }] } + }); + if (!qChallengeMember) error(404, 'Du bist kein Mitglied dieser Challenge'); + + await db.delete(challengeMember).where(eq(challengeMember.id, qChallengeMember.id)); +}); + +export const joinChallenge = command(clubChallengeInput, async ({ clubId, challengeId }) => { + const user = requireUser(); + + const clubRel = await db.query.clubMember.findFirst({ + where: { AND: [{ userId: user.id }, { clubId }] } + }); + if (!clubRel) error(401, 'Du bist kein Mitglied des Clubs'); + + await db.insert(challengeMember).values({ challengeId, userId: user.id }); +}); + +export const addDisciplines = form(addDisciplinesSchema, async ({ challengeId, discipline: items }) => { + const user = requireUser(); + const qChallenge = await db.query.challenge.findFirst({ where: { id: challengeId } }); + if (!qChallenge) error(404, 'Challenge nicht gefunden'); + + if (!(await checkAdmin(qChallenge.clubId, user.id))) error(401, 'Nicht erlaubt'); + + for (const d of items) { + await db.insert(discipline).values({ + factor: d.multiplier.toString(), + name: d.name, + challengeId + }); + } +}); + +export const deleteDiscipline = command(deleteDisciplineSchema, async ({ challengeId, disciplineId }) => { + const user = requireUser(); + const qChallenge = await db.query.challenge.findFirst({ where: { id: challengeId } }); + if (!qChallenge) error(404, 'Challenge nicht gefunden'); + + if (!(await checkAdmin(qChallenge.clubId, user.id))) error(401, 'Nicht erlaubt'); + + const qDiscipline = await db.query.discipline.findFirst({ + where: { AND: [{ id: disciplineId }, { challengeId }] } + }); + if (!qDiscipline) error(404, 'Disziplin nicht gefunden'); + + await db.delete(discipline).where(eq(discipline.id, qDiscipline.id)); +}); + +export const deleteEntry = command(deleteEntrySchema, async ({ challengeId, entryId }) => { + const user = requireUser(); + + const qEntry = await db.query.entry.findFirst({ + where: { id: entryId, challengeId } + }); + if (!qEntry) error(404); + if (qEntry.userId !== user.id) error(401); + + await db.delete(entry).where(eq(entry.id, qEntry.id)); +}); + +export const getDaysRemainingForChallengeEntries = query(challengeInput, async ({ challengeId }) => { + const qChallenge = await db.query.challenge.findFirst({ where: { id: challengeId } }); + if (!qChallenge) error(404); + return { + canAddEntries: canAddEntries(qChallenge), + daysRemaining: getDaysRemainingForEntry(qChallenge) + }; +}); diff --git a/src/lib/remote/club.remote.ts b/src/lib/remote/club.remote.ts new file mode 100644 index 0000000..75104a0 --- /dev/null +++ b/src/lib/remote/club.remote.ts @@ -0,0 +1,229 @@ +import { command, form, query } from '$app/server'; +import { db, checkAdmin } from '$lib/db'; +import { challenge, club, inviteCode, clubAdmin, clubMember } from '$lib/db/schema'; +import { requireUser } from '$lib/remote/auth.remote'; +import { error, redirect } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { z } from 'zod'; + +const clubIdSchema = z.object({ clubId: z.string() }); + +const createClubSchema = z.object({ + name: z.string().min(5) +}); + +const joinSchema = z.object({ + code: z.string().min(6) +}); + +const createChallengeFormSchema = z + .object({ + clubId: z.string(), + name: z.string().min(3), + startsAt: z.string(), + endsAt: z.string() + }) + .refine((data) => new Date(data.endsAt).getTime() > new Date(data.startsAt).getTime(), { + message: 'Enddatum muss nach dem Startdatum liegen', + path: ['endsAt'] + }); + +const editClubSchema = z.object({ + clubId: z.string(), + name: z.string().min(5) +}); + +export const ensureClubsAccess = query(async () => { + const user = requireUser(); + if (!user.completedProfile) redirect(302, '/profile/edit'); + return { user }; +}); + +export const getClubsPageData = query(async () => { + const user = requireUser(); + + const usersClubs = await db.query.clubMember.findMany({ + where: { userId: user.id }, + with: { + club: { + with: { + challenges: true, + members: true + } + } + } + }); + + return { usersClubs, user }; +}); + +export const getClubPageData = query(clubIdSchema, async ({ clubId }) => { + const user = requireUser(); + + const qClub = await db.query.club.findFirst({ + where: { id: clubId }, + with: { + challenges: { + with: { + members: true + } + }, + members: true + } + }); + + if (!qClub) error(404, 'Dieser Club existiert nicht'); + + if ( + !(await db.query.clubMember.findFirst({ + where: { clubId: qClub.id, userId: user.id } + })) + ) { + redirect(302, '/clubs'); + } + + const clubAdmin = await checkAdmin(clubId, user.id); + + return { qClub, user, clubAdmin }; +}); + +export const createClubAccess = query(async () => { + const user = requireUser(); + if (!user.superUser) redirect(302, '/clubs'); + return { user }; +}); + +export const createClub = form(createClubSchema, async ({ name }) => { + const user = requireUser(); + if (!user.superUser) redirect(302, '/clubs'); + + const [createdClub] = await db + .insert(club) + .values({ + name + }) + .returning(); + + await db.insert(clubAdmin).values({ + clubId: createdClub.id, + userId: user.id + }); + + await db.insert(clubMember).values({ + clubId: createdClub.id, + userId: user.id + }); + + redirect(302, `/clubs/${createdClub.id}`); +}); + +export const joinClubAccess = query(async () => { + const user = requireUser(); + return { user }; +}); + +export const submitJoinCode = form(joinSchema, async ({ code }) => { + redirect(302, `/clubs/join/${code}`); +}); + +export const joinClubByCode = query(z.object({ code: z.string() }), async ({ code }) => { + const user = requireUser(); + + const qClub = await db.query.inviteCode.findFirst({ where: { code } }); + if (!qClub) error(404, 'Einladungscode ungültig'); + + const alreadyJoined = await db.query.clubMember.findFirst({ + where: { AND: [{ clubId: qClub.clubId }, { userId: user.id }] } + }); + + if (!alreadyJoined) { + await db.insert(clubMember).values({ + clubId: qClub.clubId, + userId: user.id + }); + } + + redirect(302, `/clubs/${qClub.clubId}`); +}); + +export const createChallengeInClub = form(createChallengeFormSchema, async ({ clubId, name, startsAt, endsAt }) => { + const user = requireUser(); + if (!user.superUser) error(401, 'Nicht erlaubt.'); + + const [{ id: challengeId }] = await db + .insert(challenge) + .values({ + name, + startsAt: new Date(startsAt), + endsAt: new Date(endsAt), + clubId + }) + .returning({ id: challenge.id }); + + redirect(302, `/clubs/${clubId}/challenge/${challengeId}`); +}); + +export const getInviteCode = command(clubIdSchema, async ({ clubId }) => { + const user = requireUser(); + if (!(await checkAdmin(clubId, user.id))) error(401, 'Nicht erlaubt.'); + + const [{ code }] = await db + .insert(inviteCode) + .values({ + clubId + }) + .returning(); + + return { code }; +}); + +export const deleteClub = command(clubIdSchema, async ({ clubId }) => { + const user = requireUser(); + + const qClub = await db.query.club.findFirst({ + where: { id: clubId } + }); + + if (!qClub) error(404, 'Club existiert nicht'); + + const isAdmin = await checkAdmin(qClub.id, user.id); + if (!isAdmin) error(401, 'Nicht erlaubt'); + + await db.delete(club).where(eq(club.id, qClub.id)); + + redirect(302, '/clubs'); +}); + +export const leaveClub = command(clubIdSchema, async ({ clubId }) => { + const user = requireUser(); + + const qClubMember = await db.query.clubMember.findFirst({ + where: { clubId, userId: user.id } + }); + + if (!qClubMember) error(404, 'Du bist kein Mitglied dieses Clubs'); + + await db.delete(clubMember).where(eq(clubMember.id, qClubMember.id)); + + redirect(302, '/clubs'); +}); + +export const editClubDetails = form(editClubSchema, async ({ clubId, name }) => { + const user = requireUser(); + + const qClub = await db.query.club.findFirst({ + where: { id: clubId } + }); + + if (!qClub) error(404, 'Club existiert nicht'); + + const isAdmin = await checkAdmin(qClub.id, user.id); + if (!isAdmin) error(401, 'Nicht erlaubt'); + + await db + .update(club) + .set({ + name + }) + .where(eq(club.id, qClub.id)); +}); diff --git a/src/lib/remote/profile.remote.ts b/src/lib/remote/profile.remote.ts new file mode 100644 index 0000000..6e58b6c --- /dev/null +++ b/src/lib/remote/profile.remote.ts @@ -0,0 +1,40 @@ +import { form, getRequestEvent, query } from '$app/server'; +import { db } from '$lib/db'; +import { user } from '$lib/db/schema'; +import { requireUser } from '$lib/remote/auth.remote'; +import { redirect } from '@sveltejs/kit'; +import { userSetup } from '$lib/zod'; +import { eq } from 'drizzle-orm'; + +export const getProfileData = query(async () => { + return { user: requireUser() }; +}); + +export const getProfileEditData = query(async () => { + const currentUser = requireUser(); + + return { + defaults: { + gender: currentUser.gender as string | undefined, + role: currentUser.role as string | undefined, + username: currentUser.name + } + }; +}); + +export const updateProfile = form(userSetup, async ({ role, username: name, gender }) => { + const currentUser = requireUser(); + const { url } = getRequestEvent(); + + await db + .update(user) + .set({ + role, + name, + gender + }) + .where(eq(user.id, currentUser.id)); + + const redirectUrl = url.searchParams.get('redirect'); + redirect(302, redirectUrl || '/profile'); +}); diff --git a/src/lib/zod.ts b/src/lib/zod.ts index b8afee6..f772424 100644 --- a/src/lib/zod.ts +++ b/src/lib/zod.ts @@ -4,8 +4,8 @@ import { gender, roles } from './db/schema'; export const createChallenge = z .object({ name: z.string().min(3), - startsAt: z.date(), - endsAt: z.date() + startsAt: z.coerce.date(), + endsAt: z.coerce.date() }) .refine((data) => data.endsAt > data.startsAt, { message: 'endsAt must be after startsAt', @@ -22,18 +22,14 @@ export const addDisciplines = z.object({ discipline: z .object({ name: z.string(), - multiplier: z - .number({ - error: (issue) => (issue.input === undefined ? 'This field is required' : 'Not a string') - }) - .multipleOf(0.1) + multiplier: z.coerce.number().multipleOf(0.1) }) .array() }); export const newEntry = z.object({ disciplineId: z.string(), - amount: z.number().multipleOf(0.01).min(0.01), + amount: z.coerce.number().multipleOf(0.01).min(0.01), date: z.iso.date() }); diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts deleted file mode 100644 index 9322896..0000000 --- a/src/routes/+layout.server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { auth } from '$lib/auth'; -import type { LayoutServerLoad } from './$types'; - -export const load: LayoutServerLoad = async ({ request }) => { - const session = await auth.api.getSession({ - headers: request.headers - }); - - return { session }; -}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b8a3474..8e2c08e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -8,10 +8,11 @@ import { Toaster } from '$lib/components/ui/sonner'; import { page } from '$app/state'; import { dev } from '$app/environment'; + import { getLayoutSession } from '$lib/remote/auth.remote'; - let { children, data } = $props(); + let { children } = $props(); - let { session } = data; + const { session } = await getLayoutSession(); async function signOut() { await authClient(page.url.origin).signOut(); @@ -37,18 +38,14 @@
- - {session.user.name} - + {session.user.name} Profil Übersicht - Bearbeiten + Bearbeiten diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts deleted file mode 100644 index d53afb7..0000000 --- a/src/routes/+page.server.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { db, getLeaderBoard } from '$lib/db'; -import { challenge, challengeMember } from '$lib/db/schema'; -import { lte, gte, and, getColumns, eq, sql } from 'drizzle-orm'; -import type { PageServerLoad } from './$types'; -import { superValidate } from 'sveltekit-superforms'; -import { zod4 } from 'sveltekit-superforms/adapters'; -import { newEntry } from '$lib/zod'; -import { getDaysRemainingForEntry, canAddEntries } from '$lib/utils'; - -export const load: PageServerLoad = async ({ locals }) => { - const { user } = locals; - - if (user) { - const now = new Date(); - // Set to start of day (00:00:00.000) - const startOfToday = new Date(now); - startOfToday.setHours(0, 0, 0, 0); - - // Set to end of day (23:59:59.999) - const endOfToday = new Date(now); - endOfToday.setHours(23, 59, 59, 999); - - const activeChallenges = await db - .select({ - ...getColumns(challenge) - }) - .from(challengeMember) - .innerJoin(challenge, eq(challengeMember.challengeId, challenge.id)) - .where( - and( - eq(challengeMember.userId, user.id), - // Challenge starts at or before end of today - lte(challenge.startsAt, endOfToday), - // Challenge ends at or after start of today - gte(challenge.endsAt, startOfToday) - ) - ) - .groupBy(challenge.id); - - const newEntryForm = await superValidate(zod4(newEntry)); - - const challengesWithLeaderboards = activeChallenges.map(async (c) => { - const disciplines = await db.query.discipline.findMany({ - where: { - challengeId: c.id - } - }); - const leaderboard = getLeaderBoard.execute({ challengeId: c.id, limit: 5 }); - return { ...c, leaderboard, disciplines }; - }); - - // Fetch challenges still open for entries (within 2-day grace period after end date) - const gracePeriodEnd = new Date(); - gracePeriodEnd.setUTCDate(gracePeriodEnd.getUTCDate() + 2); - gracePeriodEnd.setUTCHours(23, 59, 59, 999); - - const openForEntriesChallenges = await db - .select({ - ...getColumns(challenge) - }) - .from(challengeMember) - .innerJoin(challenge, eq(challengeMember.challengeId, challenge.id)) - .where( - and( - eq(challengeMember.userId, user.id), - // Challenge has ended - lte(challenge.endsAt, endOfToday), - // Challenge is still within grace period - gte(challenge.endsAt, new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)) - ) - ) - .groupBy(challenge.id); - - const openForEntriesChallengesWithDays = openForEntriesChallenges - .filter((c) => canAddEntries(c)) - .map((c) => ({ - ...c, - daysRemaining: getDaysRemainingForEntry(c) - })); - - return { - challengesWithLeaderboards, - user, - newEntryForm, - openForEntriesChallenges: openForEntriesChallengesWithDays - }; - } -}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a61bbca..96577e0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,16 +1,14 @@ + +{@render children()} diff --git a/src/routes/clubs/+page.server.ts b/src/routes/clubs/+page.server.ts deleted file mode 100644 index 4750d67..0000000 --- a/src/routes/clubs/+page.server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { db } from '$lib/db'; -import type { PageServerLoad } from './$types'; -import { getUser } from '$lib/utils'; - -export const load: PageServerLoad = async ({ locals, url }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - - const usersClubs = await db.query.clubMember.findMany({ - where: { userId: user.id }, - with: { - club: { - with: { - challenges: true, - members: true - } - } - } - }); - - return { usersClubs, user }; -}; diff --git a/src/routes/clubs/+page.svelte b/src/routes/clubs/+page.svelte index 35af88a..01845e4 100644 --- a/src/routes/clubs/+page.svelte +++ b/src/routes/clubs/+page.svelte @@ -2,9 +2,9 @@ import Button from '$lib/components/ui/button/button.svelte'; import * as Card from '$lib/components/ui/card'; import { CirclePlus, PlusCircle } from '@lucide/svelte'; + import { getClubsPageData } from '$lib/remote/club.remote'; - let { data } = $props(); - + const data = await getClubsPageData(); const { usersClubs } = data; diff --git a/src/routes/clubs/[clubId]/+page.server.ts b/src/routes/clubs/[clubId]/+page.server.ts deleted file mode 100644 index ec78e4a..0000000 --- a/src/routes/clubs/[clubId]/+page.server.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { db, checkAdmin } from '$lib/db'; -import { eq } from 'drizzle-orm'; -import type { PageServerLoad, Actions } from './$types'; -import { club, challenge, inviteCode, clubMember } from '$lib/db/schema'; -import { error } from '@sveltejs/kit'; -import { getUser } from '$lib/utils'; -import { createChallenge, editClub } from '$lib/zod'; -import { fail, redirect } from '@sveltejs/kit'; -import { superValidate } from 'sveltekit-superforms'; -import { zod4 } from 'sveltekit-superforms/adapters'; - -export const load: PageServerLoad = async ({ params, locals, url }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - const qClub = await db.query.club.findFirst({ - where: { id: params.clubId }, - with: { - challenges: { - with: { - members: true - } - }, - members: true - } - }); - - if (!qClub) return error(404, 'Dieser Club existiert nicht'); - - if ( - !(await db.query.clubMember.findFirst({ - where: { clubId: qClub.id, userId: user.id } - })) - ) - return redirect(302, '/clubs'); - - const createForm = await superValidate(zod4(createChallenge)); - - const editClubForm = await superValidate(zod4(editClub), { - defaults: { - name: qClub.name - } - }); - - const clubAdmin = await checkAdmin(params.clubId, user.id); - - return { qClub, createForm, user, clubAdmin, editClubForm }; -}; - -export const actions: Actions = { - createChallenge: async ({ locals, request, url, params }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - - const form = await superValidate(request, zod4(createChallenge)); - - if (!form.valid) { - return fail(400, { form }); - } - - if (!user.superUser) return error(401, 'Nicht erlaubt.'); - - const { endsAt, name, startsAt } = form.data as { - name: string; - startsAt: Date | string; - endsAt: Date | string; - }; - - const [{ id: challengeId }] = await db - .insert(challenge) - .values({ - name, - startsAt: new Date(startsAt), - endsAt: new Date(endsAt), - clubId: params.clubId - }) - .returning({ id: challenge.id }); - - redirect(302, `/clubs/${params.clubId}/challenge/${challengeId}`); - }, - getCode: async ({ locals, params, url }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - if (!(await checkAdmin(params.clubId, user.id))) return error(401, 'Nicht erlaubt.'); - - const [{ code }] = await db - .insert(inviteCode) - .values({ - clubId: params.clubId - }) - .returning(); - - return { code }; - }, - deleteClub: async ({ locals, url, params }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - - // query club from db - const qClub = await db.query.club.findFirst({ - where: { id: params.clubId } - }); - - // error if no club - if (!qClub) return error(404, 'Club existiert nicht'); - - // check if user is admin of club - const isAdmin = await checkAdmin(qClub.id, user.id); - - // error if not admin - if (!isAdmin) return error(401, 'Nicht erlaubt'); - - // actually delete - await db.delete(club).where(eq(club.id, qClub.id)); - - return redirect(302, '/clubs'); - }, - leave: async ({ locals, url, params }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - - const qClubMember = await db.query.clubMember.findFirst({ - where: { clubId: params.clubId, userId: user.id } - }); - - if (!qClubMember) return error(404, 'Du bist kein Mitglied dieses Clubs'); - - await db.delete(clubMember).where(eq(clubMember.id, qClubMember.id)); - - return redirect(302, '/clubs'); - }, - edit: async ({ locals, url, params, request }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - - const form = await superValidate(request, zod4(editClub)); - - if (!form.valid) return fail(400, { form }); - - // query club from db - const qClub = await db.query.club.findFirst({ - where: { id: params.clubId } - }); - - // error if no club - if (!qClub) return error(404, 'Club existiert nicht'); - - // check if user is admin of club - const isAdmin = await checkAdmin(qClub.id, user.id); - - // error if not admin - if (!isAdmin) return error(401, 'Nicht erlaubt'); - - // actually delete - await db - .update(club) - .set({ - name: form.data.name - }) - .where(eq(club.id, qClub.id)); - - return { form }; - } -}; diff --git a/src/routes/clubs/[clubId]/+page.svelte b/src/routes/clubs/[clubId]/+page.svelte index a5ec378..8c0e1b1 100644 --- a/src/routes/clubs/[clubId]/+page.svelte +++ b/src/routes/clubs/[clubId]/+page.svelte @@ -1,58 +1,71 @@
-

Aktive Challenges

@@ -109,9 +121,7 @@ > -

- {challenge.members.length} Teilnehmer -

+

{challenge.members.length} Teilnehmer

@@ -120,7 +130,6 @@ {/each}
- {#if pastChallenges.length > 0}

Vergangene Challenges

@@ -136,9 +145,7 @@ > -

- {challenge.members.length} Teilnehmer -

+

{challenge.members.length} Teilnehmer

@@ -147,10 +154,6 @@ {/if}
- - - - {#snippet challengeSheet()} @@ -165,40 +168,29 @@ -
- - - {#snippet children({ props })} - Name - - {/snippet} - - - - - - {#snippet children()} - Dauer der Challenge - Bitte Enddatum im Stil "bis", anstatt "bis und mit" wählen, weil die Endzeit auf - 00:00 gesetzt wird. - - {/snippet} - - - - Erstellen + + + + Name + + + + + Dauer der Challenge + Bitte Enddatum im Stil "bis", anstatt "bis und mit" wählen, weil die Endzeit auf + 00:00 gesetzt wird. + + + + +
@@ -218,7 +210,14 @@ > {#if !inviteCode} - + {:else}
@@ -244,16 +243,13 @@ Club bearbeiten -
- - - {#snippet children({ props })} - Name - - {/snippet} - - - + + + + Name + + +
@@ -275,8 +271,11 @@ Abbrechen - Löschen { + await deleteClub({ clubId }); + }} + class={buttonVariants({ variant: 'destructive' })}>Löschen @@ -294,8 +293,11 @@ Abbrechen - Verlassen { + await leaveClub({ clubId }); + }} + class={buttonVariants({ variant: 'destructive' })}>Verlassen diff --git a/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.server.ts b/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.server.ts deleted file mode 100644 index cae5731..0000000 --- a/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.server.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { checkAdmin, db } from '$lib/db'; -import { zod4 } from 'sveltekit-superforms/adapters'; - -import type { LayoutServerLoad } from './$types'; - -import { error, redirect } from '@sveltejs/kit'; -import { getUser } from '$lib/utils'; -import { superValidate } from 'sveltekit-superforms'; - -import { createChallenge } from '$lib/zod'; - -export const load: LayoutServerLoad = async ({ params, locals, url }) => { - const user = getUser({ locals, redirectUrl: url.pathname }); - const { challengeId } = params; - - if (!user.completedProfile) return redirect(302, '/profile/edit'); - - const qChallenge = await db.query.challenge.findFirst({ - where: { id: challengeId }, - with: { - members: true, - entries: { - with: { - user: true, - discipline: true - } - }, - disciplines: true - } - }); - - if (!qChallenge) return error(404, 'Challenge nicht gefunden'); - - const currentUserChallenge = await db.query.challengeMember.findFirst({ - where: { challengeId, userId: user.id } - }); - - const currentUserClub = await db.query.clubMember.findFirst({ - where: { clubId: qChallenge.clubId, userId: user.id } - }); - - if (!currentUserClub) return redirect(302, '/clubs'); - - const clubAdmin = await checkAdmin(params.clubId, user.id); - - const challengePath = `/clubs/${params.clubId}/challenge/${params.challengeId}`; - - const editForm = await superValidate(zod4(createChallenge)); - - return { - challenge: qChallenge, - user, - currentUserChallenge, - clubAdmin, - challengePath, - editForm - }; -}; diff --git a/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.svelte b/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.svelte index 9014fb3..a7e2cf1 100644 --- a/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.svelte +++ b/src/routes/clubs/[clubId]/challenge/[challengeId]/+layout.svelte @@ -1,68 +1,70 @@ @@ -74,10 +76,7 @@ {prettyDate(challenge.startsAt)} - {prettyDate(challenge.endsAt)}

{#if canStillAddEntries && daysRemaining > 0} - - {daysRemaining} - {daysRemaining === 1 ? 'Tag' : 'Tage'} offen - + {daysRemaining} {daysRemaining === 1 ? 'Tag' : 'Tage'} offen {/if}
@@ -90,7 +89,13 @@ {#if currentUserChallenge} {@render leaveDialog()} {:else} - + {/if} @@ -99,79 +104,52 @@ {#if !currentUserChallenge}

Du bist kein Mitglied dieser Challenge.

{:else} -