feat: mvp

This commit is contained in:
2025-11-26 21:48:06 +01:00
commit c1521af887
57 changed files with 8140 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
</script>
<div class="flex items-center justify-center grow">
<h3>{page.status}: {page.error?.message ?? 'Upsi, da isch öppis kaputt gange...'}</h3>
</div>
+10
View File
@@ -0,0 +1,10 @@
import { getSession as getSession } from '$lib/auth';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => {
const session = await getSession(event);
return {
session
};
};
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import {
Navbar,
NavBrand,
NavLi,
NavUl,
NavHamburger,
DropdownItem,
Button
} from 'flowbite-svelte';
import type { LayoutProps } from './$types';
import { signIn, signOut } from '@auth/sveltekit/client';
import UserDisplay from '$lib/components/UserDisplay.svelte';
let { data, children }: LayoutProps = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<Navbar class="border-b border-gray-200 dark:border-gray-700">
<NavBrand href="/">
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">aktiteil</span
>
</NavBrand>
<NavHamburger />
<NavUl>
<NavLi href="/">Dehei</NavLi>
<NavLi href="/akti">Neui Akti</NavLi>
</NavUl>
{#if data.session?.user}
<UserDisplay user={data.session.user}>
<DropdownItem>
<Button onclick={() => signOut()}>Sign out</Button>
</DropdownItem>
</UserDisplay>
{:else}
<Button onclick={() => signIn('nextcloud')}>Signin</Button>
{/if}
</Navbar>
<main class="p-5 sm:px-20 2xl:px-60 flex flex-col h-full grow">
{@render children()}
</main>
+21
View File
@@ -0,0 +1,21 @@
import { db } from '$lib/server/db';
import { aktis, ratings } from '$lib/server/db/schema';
import { avg, eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const a = 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);
return {
aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined }))
};
};
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import AktiCard from '$lib/components/akti/AktiCard.svelte';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<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)}
<AktiCard {akti}></AktiCard>
{/each}
</div>
+38
View File
@@ -0,0 +1,38 @@
import { redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { extractFormData } from '$lib/extractFormData';
import { resolve } from '$app/paths';
import * as v from 'valibot';
import { ensureAuth } from '$lib/auth';
import { db } from '$lib/server/db';
import { aktis } from '$lib/server/db/schema';
export const load: PageServerLoad = async (event) => {
await ensureAuth(event);
return {};
};
export const actions = {
default: async (event) => {
const user = await ensureAuth(event);
const akti = (
await extractFormData(
event.request,
v.object({
title: v.pipe(v.string(), v.minLength(5)),
summary: v.pipe(v.string(), v.minLength(5)),
body: v.pipe(v.string(), v.minLength(5))
})
)
).data;
if (!akti) return {};
const res = await db
.insert(aktis)
.values({ ...akti, author: user.id! })
.returning({ id: aktis.id });
return redirect(303, resolve(`/akti/[aktiId]`, { aktiId: res[0].id }));
}
} satisfies Actions;
+5
View File
@@ -0,0 +1,5 @@
<script lang="ts">
import AktiEditor from '$lib/components/akti/AktiEditor.svelte';
</script>
<AktiEditor akti={{ title: '', summary: '', body: '' }} />
+66
View File
@@ -0,0 +1,66 @@
import { db } from '$lib/server/db';
import { aktis, ratings } from '$lib/server/db/schema';
import { error, redirect, type Actions } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types';
import { ensureAuth } from '$lib/auth';
import { extractFormData } from '$lib/extractFormData';
import * as v from 'valibot';
import { resolve } from '$app/paths';
export const load: PageServerLoad = async (event) => {
const akti = await db.query.aktis.findFirst({
where: eq(aktis.id, event.params.aktiId),
with: { author: true }
});
const r = await db.query.ratings.findMany({
with: { user: true },
where: eq(ratings.aktiId, event.params.aktiId)
});
if (!akti) {
error(404, { message: 'Die Akti gits garnid, sorry...' });
}
return {
akti,
ratings: r
};
};
export const actions = {
default: async (event) => {
const user = await ensureAuth(event);
if (!event.params.aktiId) return error(404);
const akti = await db
.select({ id: aktis.id, version: aktis.version, author: aktis.author })
.from(aktis)
.limit(1)
.where(eq(aktis.id, event.params.aktiId));
if (!akti || akti.length == 0) return error(404);
if (akti[0].author != user.id) return error(403);
const changeRequest = (
await extractFormData(
event.request,
v.object({
title: v.pipe(v.string(), v.minLength(5)),
summary: v.pipe(v.string(), v.minLength(5)),
body: v.pipe(v.string(), v.minLength(5))
})
)
).data;
if (!changeRequest) return error(400);
const res = await db
.insert(aktis)
.values({ ...changeRequest, author: user.id, version: akti[0].version + 1 })
.returning({ id: aktis.id });
return redirect(303, resolve(`/akti/[aktiId]`, { aktiId: res[0].id }));
}
} satisfies Actions;
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { Button } from 'flowbite-svelte';
import type { PageProps } from './$types';
import { EditOutline, CloseOutline } from 'flowbite-svelte-icons';
import AktiEditor from '$lib/components/akti/AktiEditor.svelte';
import UserDisplay from '$lib/components/UserDisplay.svelte';
let { data }: PageProps = $props();
let edit = $state(false);
</script>
<div class="flex justify-between">
<h2>{data.akti?.title} <span class="text-xs text-gray-400">v{data.akti.version}</span></h2>
{#if data.session?.user?.id === data.akti.author?.id && data.akti.author?.id}
<Button onclick={() => (edit = !edit)} color={edit ? 'gray' : 'primary'}>
{#if edit}
<CloseOutline class="shrink-0 h-6 w-6" />
{:else}
<EditOutline class="shrink-0 h-6 w-6 -mr-0.5 ml-0.5" />
{/if}
</Button>
{:else}
<div class="flex gap-5 items-center">
<p>gschribe vo:</p>
<UserDisplay user={data.akti.author} />
</div>
{/if}
</div>
{#if edit}
<AktiEditor akti={data.akti} />
{:else}
<div class="p-5 my-5 bg-gray-200 rounded-md">
<h3 class="mb-2">Zämefassig</h3>
<p>{data.akti.summary}</p>
</div>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html data.akti.body}
{/if}