feat: mvp

This commit is contained in:
2025-11-26 21:48:06 +01:00
commit 5ab04b9acb
58 changed files with 14997 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.git
.env

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
DATABASE_URL="postgres://root:mysecretpassword@localhost:5432/local"
AUTH_SECRET=adf
AUTH_TRUST_HOST=true
AUTH_NEXTCLOUD_ID=asdf
AUTH_NEXTCLOUD_SECRET=adsf
AUTH_NEXTCLOUD_ISSUER="https://cloud.schreifuchs.ch"
AUTH_TRUST_HOST=true

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

9
.husky/commit-msg Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
if ! head -1 "$1" | grep -qE "^(feat|fix|chore|docs|test|style|refactor|perf|build|ci|revert)(\(.+?\))?: .{1,}$"; then
echo "Aborting commit. Your commit message is invalid. Please use conventional commits" >&2
exit 1
fi
if ! head -1 "$1" | grep -qE "^.{1,88}$"; then
echo "Aborting commit. Your commit message is too long." >&2
exit 1
fi

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
pnpm run format
pnpm run lint

9
.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

15
.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM node:25-trixie AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# RUN corepack enable
RUN npm install -g pnpm
RUN apt-get update && \
apt-get install -y python3 build-essential
ENV CI=true
ENV npm_config_build_from_source=true
WORKDIR /app
COPY ./package.json ./
COPY ./pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM base AS build
COPY . /app
ENV DATABASE_URL=build.db
COPY .env.example .env
RUN pnpm run build
FROM base
WORKDIR /app
COPY --from=build /app/build .
COPY ./drizzle ./drizzle
COPY ./drizzle.config.ts ./drizzle.config.ts
COPY ./start.sh ./start.sh
EXPOSE 3000
CMD [ "sh", "start.sh" ]

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# Aktiteil
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 in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `pnpm install` ), 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/kitk/adapters) for your target environment.

38
compose.yaml Normal file
View File

@@ -0,0 +1,38 @@
services:
db:
image: postgres
restart: always
ports:
- 5432:5432
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: local
volumes:
- pgdata:/var/lib/postgresql
frontend:
profiles:
- frontend
build: .
ports:
- 5173:3000
environment:
DATABASE_URL: 'postgres://root:mysecretpassword@db:5432/local'
ORIGIN: 'http://localhost:5173'
AUTH_SECRET: ${AUTH_SECRET}
AUTH_TRUST_HOST: true
AUTH_NEXTCLOUD_ID: ${AUTH_NEXTCLOUD_ID}
AUTH_NEXTCLOUD_SECRET: ${AUTH_NEXTCLOUD_SECRET}
AUTH_NEXTCLOUD_ISSUER: ${AUTH_NEXTCLOUD_ISSUER}
depends_on:
- db
develop:
watch:
- path: .
action: rebuild
target: /app
volumes:
pgdata:

12
drizzle.config.ts Normal file
View File

@@ -0,0 +1,12 @@
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/index.ts',
dialect: 'postgresql',
dbCredentials: { url: process.env.DATABASE_URL },
verbose: true,
strict: true,
casing: 'snake_case'
});

View File

@@ -0,0 +1,69 @@
CREATE TABLE "account" (
"userId" text NOT NULL,
"type" text NOT NULL,
"provider" text NOT NULL,
"providerAccountId" text NOT NULL,
"refresh_token" text,
"access_token" text,
"expires_at" integer,
"token_type" text,
"scope" text,
"id_token" text,
"session_state" text
);
--> statement-breakpoint
CREATE TABLE "authenticator" (
"credentialID" text NOT NULL,
"userId" text NOT NULL,
"providerAccountId" text NOT NULL,
"credentialPublicKey" text NOT NULL,
"counter" integer NOT NULL,
"credentialDeviceType" text NOT NULL,
"credentialBackedUp" boolean NOT NULL,
"transports" text,
CONSTRAINT "authenticator_credentialID_unique" UNIQUE("credentialID")
);
--> statement-breakpoint
CREATE TABLE "session" (
"sessionToken" text PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
"expires" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "verificationToken" (
"identifier" text NOT NULL,
"token" text NOT NULL,
"expires" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text,
"email" text,
"emailVerified" timestamp,
"image" text,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "akti" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"summary" text NOT NULL,
"body" text NOT NULL,
"user_id" text
);
--> statement-breakpoint
CREATE TABLE "rating" (
"akti_id" uuid NOT NULL,
"user_id" text NOT NULL,
"rating" real NOT NULL,
"comment" text,
CONSTRAINT "rating_akti_id_user_id_pk" PRIMARY KEY("akti_id","user_id")
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "authenticator" ADD CONSTRAINT "authenticator_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "akti" ADD CONSTRAINT "akti_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "rating" ADD CONSTRAINT "rating_akti_id_akti_id_fk" FOREIGN KEY ("akti_id") REFERENCES "public"."akti"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "rating" ADD CONSTRAINT "rating_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "akti" ADD COLUMN "version" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "rating" ADD COLUMN "akti_version" integer NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "rating" DROP CONSTRAINT "rating_akti_id_user_id_pk";--> statement-breakpoint
ALTER TABLE "rating" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "akti" ALTER COLUMN "user_id" SET NOT NULL;

View File

@@ -0,0 +1,421 @@
{
"id": "edd8f7a0-6cbd-41fc-8bf6-a381230a2085",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.authenticator": {
"name": "authenticator",
"schema": "",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"nullsNotDistinct": false,
"columns": ["credentialID"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verificationToken": {
"name": "verificationToken",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"emailVerified": {
"name": "emailVerified",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.akti": {
"name": "akti",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"akti_user_id_user_id_fk": {
"name": "akti_user_id_user_id_fk",
"tableFrom": "akti",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rating": {
"name": "rating",
"schema": "",
"columns": {
"akti_id": {
"name": "akti_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"rating": {
"name": "rating",
"type": "real",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"rating_akti_id_akti_id_fk": {
"name": "rating_akti_id_akti_id_fk",
"tableFrom": "rating",
"tableTo": "akti",
"columnsFrom": ["akti_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"rating_user_id_user_id_fk": {
"name": "rating_user_id_user_id_fk",
"tableFrom": "rating",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"rating_akti_id_user_id_pk": {
"name": "rating_akti_id_user_id_pk",
"columns": ["akti_id", "user_id"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,434 @@
{
"id": "c142665c-e8ff-4d27-8409-b23ca6b82039",
"prevId": "edd8f7a0-6cbd-41fc-8bf6-a381230a2085",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.authenticator": {
"name": "authenticator",
"schema": "",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"nullsNotDistinct": false,
"columns": ["credentialID"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verificationToken": {
"name": "verificationToken",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"emailVerified": {
"name": "emailVerified",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.akti": {
"name": "akti",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {
"akti_user_id_user_id_fk": {
"name": "akti_user_id_user_id_fk",
"tableFrom": "akti",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rating": {
"name": "rating",
"schema": "",
"columns": {
"akti_id": {
"name": "akti_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"rating": {
"name": "rating",
"type": "real",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"akti_version": {
"name": "akti_version",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"rating_akti_id_akti_id_fk": {
"name": "rating_akti_id_akti_id_fk",
"tableFrom": "rating",
"tableTo": "akti",
"columnsFrom": ["akti_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"rating_user_id_user_id_fk": {
"name": "rating_user_id_user_id_fk",
"tableFrom": "rating",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"rating_akti_id_user_id_pk": {
"name": "rating_akti_id_user_id_pk",
"columns": ["akti_id", "user_id"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,436 @@
{
"id": "88b612dd-f1df-4eaf-a9d9-cd29db8996b9",
"prevId": "c142665c-e8ff-4d27-8409-b23ca6b82039",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.authenticator": {
"name": "authenticator",
"schema": "",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"nullsNotDistinct": false,
"columns": ["credentialID"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verificationToken": {
"name": "verificationToken",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"emailVerified": {
"name": "emailVerified",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.akti": {
"name": "akti",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {
"akti_user_id_user_id_fk": {
"name": "akti_user_id_user_id_fk",
"tableFrom": "akti",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rating": {
"name": "rating",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"akti_id": {
"name": "akti_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"rating": {
"name": "rating",
"type": "real",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"akti_version": {
"name": "akti_version",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"rating_akti_id_akti_id_fk": {
"name": "rating_akti_id_akti_id_fk",
"tableFrom": "rating",
"tableTo": "akti",
"columnsFrom": ["akti_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"rating_user_id_user_id_fk": {
"name": "rating_user_id_user_id_fk",
"tableFrom": "rating",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,436 @@
{
"id": "8f81af0f-3a4e-4fb8-b7f1-cf4e6eec7ecb",
"prevId": "88b612dd-f1df-4eaf-a9d9-cd29db8996b9",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.authenticator": {
"name": "authenticator",
"schema": "",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"nullsNotDistinct": false,
"columns": ["credentialID"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verificationToken": {
"name": "verificationToken",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"emailVerified": {
"name": "emailVerified",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.akti": {
"name": "akti",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {
"akti_user_id_user_id_fk": {
"name": "akti_user_id_user_id_fk",
"tableFrom": "akti",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rating": {
"name": "rating",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"akti_id": {
"name": "akti_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"rating": {
"name": "rating",
"type": "real",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"akti_version": {
"name": "akti_version",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"rating_akti_id_akti_id_fk": {
"name": "rating_akti_id_akti_id_fk",
"tableFrom": "rating",
"tableTo": "akti",
"columnsFrom": ["akti_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"rating_user_id_user_id_fk": {
"name": "rating_user_id_user_id_fk",
"tableFrom": "rating",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,34 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1763928082321,
"tag": "0000_busy_zarda",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1763997401452,
"tag": "0001_worthless_shiva",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1763999378258,
"tag": "0002_demonic_glorian",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1764000151184,
"tag": "0003_tranquil_iron_monger",
"breakpoints": true
}
]
}

38
eslint.config.js Normal file
View File

@@ -0,0 +1,38 @@
import { fileURLToPath } from 'node:url';
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 ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

6857
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "aktiteil",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "husky",
"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 .",
"db:start": "docker compose up",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
"@auth/core": "0.41.1",
"@auth/sveltekit": "1.11.1",
"@eslint/compat": "^1.4.1",
"@eslint/js": "^9.39.1",
"@flowbite-svelte-plugins/texteditor": "^0.25.6",
"@sveltejs/adapter-node": "^5.4.0",
"@sveltejs/kit": "^2.49.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.17",
"@tiptap/core": "3.7.2",
"@types/node": "^20.19.25",
"drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7",
"eslint": "^9.39.1",
"eslint-plugin-svelte": "^3.13.0",
"flowbite": "^4.0.1",
"flowbite-svelte": "^1.28.1",
"flowbite-svelte-icons": "^3.0.0",
"globals": "^16.5.0",
"husky": "^9.1.7",
"lowlight": "^3.3.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.43.14",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"typescript-eslint": "^8.47.0",
"vite": "^7.2.4"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.11.1",
"@sveltejs/adapter-auto": "^7.0.0",
"postgres": "^3.4.7",
"sanitize-html": "^2.17.0",
"tailwind-merge": "^3.4.0",
"valibot": "^1.1.0"
}
}

5173
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

74
src/app.css Normal file
View File

@@ -0,0 +1,74 @@
@import 'tailwindcss';
@plugin 'flowbite/plugin';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary-50: #fff5f2;
--color-primary-100: #fff1ee;
--color-primary-200: #ffe4de;
--color-primary-300: #ffd5cc;
--color-primary-400: #ffbcad;
--color-primary-500: #fe795d;
--color-primary-600: #ef562f;
--color-primary-700: #eb4f27;
--color-primary-800: #cc4522;
--color-primary-900: #a5371b;
--color-secondary-50: #f0f9ff;
--color-secondary-100: #e0f2fe;
--color-secondary-200: #bae6fd;
--color-secondary-300: #7dd3fc;
--color-secondary-400: #38bdf8;
--color-secondary-500: #0ea5e9;
--color-secondary-600: #0284c7;
--color-secondary-700: #0369a1;
--color-secondary-800: #075985;
--color-secondary-900: #0c4a6e;
}
@source "../node_modules/flowbite-svelte/dist";
@source "../node_modules/flowbite-svelte-icons/dist";
@layer base {
/* disable chrome cancel button */
input[type='search']::-webkit-search-cancel-button {
display: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
line-height: 1.2;
}
h1 {
@apply text-4xl md:text-5xl lg:text-6xl;
letter-spacing: -0.02em;
}
h2 {
@apply text-3xl md:text-4xl lg:text-5xl;
letter-spacing: -0.02em;
}
h3 {
@apply text-2xl md:text-3xl lg:text-4xl;
}
h4 {
@apply text-xl md:text-2xl lg:text-3xl;
}
h5 {
@apply text-lg md:text-xl lg:text-2xl;
}
h6 {
@apply text-base md:text-lg lg:text-xl;
}
}

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

14
src/app.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body
data-sveltekit-preload-data="hover"
class="dark:bg-gray-800 dark:text-gray-200 min-h-screen flex flex-col"
>
%sveltekit.body%
</body>
</html>

17
src/auth.ts Normal file
View File

@@ -0,0 +1,17 @@
import { SvelteKitAuth } from '@auth/sveltekit';
import Nextcloud from '@auth/sveltekit/providers/nextcloud';
import { env } from '$env/dynamic/private';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '$lib/server/db';
export const { handle, signIn, signOut } = SvelteKitAuth({
trustHost: env.AUTH_TRUST_HOST === 'true',
adapter: DrizzleAdapter(db),
providers: [
Nextcloud({
clientId: env.AUTH_NEXTCLOUD_ID,
clientSecret: env.AUTH_NEXTCLOUD_SECRET,
issuer: env.AUTH_NEXTCLOUD_ISSUER
})
]
});

1
src/hooks.server.ts Normal file
View File

@@ -0,0 +1 @@
export { handle } from './auth';

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

43
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { Session, User } from '@auth/sveltekit';
import { error } from '@sveltejs/kit';
import { db } from './server/db';
import { users } from './server/db/schema';
import { eq } from 'drizzle-orm';
interface Event {
locals: {
auth(): Promise<Session | null>;
};
}
interface UserWithId extends User {
id: string;
email: string;
}
export async function ensureAuth(event: Event): Promise<UserWithId> {
const session = await getSession(event);
if (!session) error(401, { message: 'Du muesch di zersch iiloge' });
const user = session?.user;
if (!user || !user.email || !user.id) {
error(401, { message: 'Du muesch di zersch iiloge' });
}
return { ...user, id: user.id, email: user.email }; // weird thingamajig so that ts compiler is happy
}
export async function getSession(event: Event) {
const session = await event.locals.auth();
if (!session) return null;
if (!session.user) error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' });
const res = await db
.select({ id: users.id })
.from(users)
.limit(1)
.where(eq(users.email, session.user.email ?? 'eaf9302d-9525-4f3e-8147-9620d2076f67')); //uuid as default to find nothing
if (!res[0]?.id) {
error(403, { message: 'Di gits garnid. Vilich nomau usloge u iiloge?' });
}
return { expires: session.expires, user: { ...session.user, id: res[0].id } };
}

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { FormatButtonGroup, TextEditor } from '@flowbite-svelte-plugins/texteditor';
import type { Editor } from '@tiptap/core';
import { Helper } from 'flowbite-svelte';
let {
value = $bindable('Hello World'),
name,
required = false,
minlength
}: { value?: string; name?: string; required?: boolean; minlength?: number } = $props();
let editorInstance = $state<Editor | null>(null);
let updating: boolean = false;
$effect(() => {
if (updating) {
updating = false;
return;
}
// Only update content if it is actually different to prevent cursor jumping
if (editorInstance && editorInstance.getHTML() !== value) {
editorInstance.commands.setContent(value);
}
});
$effect(() => {
if (!editorInstance) return;
editorInstance.on('update', (editor) => {
updating = true;
const content = editor.editor.getHTML();
errorMessage = '';
if (editor.editor.isEmpty) {
value = '';
} else {
value = content;
}
});
});
let errorMessage = $state('');
// Derived state to easily toggle classes
</script>
<div class="relative group">
<TextEditor
bind:editor={editorInstance}
content="<p></p> <p></p>"
contentprops={{ id: 'formats-ex' }}
>
<FormatButtonGroup editor={editorInstance} />
</TextEditor>
{#if name}
<input
type="text"
{name}
bind:value
{required}
minlength={minlength ? minlength + 6 : undefined}
tabindex="-1"
style="opacity: 0; position: absolute; pointer-events: none; z-index: -1; bottom: 0; left: 50%; height: 0; width: 0;"
oninvalid={(e) => {
const target = e.target as HTMLInputElement;
// 1. Read the message (e.g., "Please fill out this field.")
errorMessage = target.validationMessage;
editorInstance?.commands.focus();
}}
/>
{/if}
</div>
{#if errorMessage}
<Helper class="mt-2" color="red">
{errorMessage}
</Helper>
{/if}

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import type { User } from '@auth/sveltekit';
import { Avatar, Dropdown } from 'flowbite-svelte';
import type { Snippet } from 'svelte';
import { twMerge, type ClassNameValue } from 'tailwind-merge';
let {
user,
children,
class: className
}: { user: User; children?: Snippet; class?: ClassNameValue } = $props();
let classList = $derived(twMerge('flex items-center gap-5', className));
</script>
{#if children}
<button class={classList}>
<Avatar src={user.image ?? undefined} />
<p>{user.name}</p>
</button>
<Dropdown simple>
{@render children?.()}
</Dropdown>
{:else}
<span class={classList}>
<Avatar src={user.image ?? undefined} />
<p>{user.name}</p>
</span>
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Rating } from 'flowbite-svelte';
interface Akti {
id: string;
title: string;
summary: string;
rating?: number;
}
let { akti }: { akti: Akti } = $props();
</script>
<a
href={resolve(`/akti/${akti.id}`)}
class="w-full bg-white border border-gray-200 dark:bg-gray-800 dark:border-gray-700 shadow-md flex flex-col items-start p-5 gap-5 hover:shadow-lg transition-shadow duration-200"
>
<h4>{akti.title}</h4>
<p class="text-left">{akti.summary}</p>
{#if akti.rating}
<Rating id="example-1b" total={5} size={30} rating={akti.rating} class="self-end mt-auto" />
{/if}
</a>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { Button, Input, Label, Textarea } from 'flowbite-svelte';
import RichText from '../RichText.svelte';
interface Akti {
title: string;
summary: string;
body: string;
}
let { akti }: { akti: Akti } = $props();
</script>
<form method="POST" class="flex flex-col gap-5">
<div>
<Label>Titu</Label>
<Input value={akti.title} type="text" name="title" required minlength={5} />
</div>
<div>
<Label>Zämefassig</Label>
<Textarea value={akti.summary} name="summary" required minlength={5} class="w-full min-h-40" />
</div>
<div>
<Label>Inhaut</Label>
<RichText value={akti.body} name="body" required />
</div>
<Button type="submit" class="grow-0 self-end">Spichere</Button>
</form>

View File

@@ -0,0 +1,62 @@
/*
* Code from: https://jovianmoon.io/posts/sveltekit-form-validation-with-valibot
*/
import { dev } from '$app/environment';
import * as v from 'valibot';
export const extractFormData = async <TInput = unknown, TOutput = TInput>(
request: Request,
schema: v.BaseSchema<TInput, TOutput, v.BaseIssue<unknown>>
): Promise<{
data: TOutput | undefined;
error: string | null;
}> => {
try {
const formData = await request.formData();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: Record<string, any> = {};
// Convert form data to an object with proper handling of multiple values
formData.forEach((value, key) => {
// Case 1: First time encountering this key
if (result[key] === undefined) {
result[key] = value;
}
// Case 2: Key exists and is already an array, add new value
else if (Array.isArray(result[key])) {
result[key].push(value);
}
// Case 3: Key exists but isn't an array yet, convert to array with both values
else {
result[key] = [result[key], value];
}
});
const validation = v.safeParse(schema, result);
if (!validation.success) {
if (dev) {
console.error('Validation errors:');
for (const error of validation.issues) {
console.error(`- ${error.message}`);
}
}
if (validation.issues && validation.issues.length > 0) {
return {
data: undefined,
error: validation.issues.map((issue) => issue.message).join(', ')
};
}
return {
data: undefined,
error: 'Error validating form submission, please check everything carefully.'
};
}
return { data: validation.output as TOutput, error: null };
} catch (error) {
if (dev) {
console.error(`Error extracting form data: ${error}`);
}
return { data: undefined, error: `Error extracting form data: ${error}` };
}
};

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import { env } from '$env/dynamic/private';
import { building } from '$app/environment';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = building ? postgres() : postgres(env.DATABASE_URL);
export const db = drizzle(client, { schema, casing: 'snake_case' });

View File

@@ -0,0 +1,49 @@
import { relations, sql } from 'drizzle-orm';
import { integer, pgTable, real, text, uuid } from 'drizzle-orm/pg-core';
import { users } from './user';
export const aktis = pgTable('akti', {
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey(),
title: text().notNull(),
summary: text().notNull(),
body: text().notNull(),
author: text('user_id')
.notNull()
.references(() => users.id),
version: integer('version').notNull().default(1)
});
export const ratings = pgTable('rating', {
id: uuid()
.default(sql`gen_random_uuid()`)
.primaryKey(),
aktiId: uuid()
.notNull()
.references(() => aktis.id, { onDelete: 'cascade' }),
userId: text()
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
rating: real().notNull(),
comment: text(),
aktiVersion: integer('akti_version').notNull()
});
export const aktisRelations = relations(aktis, ({ one }) => ({
author: one(users, {
fields: [aktis.author],
references: [users.id]
})
}));
export const ratingsRelations = relations(ratings, ({ one }) => ({
akti: one(aktis, {
fields: [ratings.aktiId],
references: [aktis.id]
}),
user: one(users, {
fields: [ratings.userId],
references: [users.id]
})
}));

View File

@@ -0,0 +1,76 @@
import { boolean, integer, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
import type { AdapterAccountType } from '@auth/core/adapters';
import { users } from './user';
export const accounts = pgTable(
'account',
{
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
type: text('type').$type<AdapterAccountType>().notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: integer('expires_at'),
token_type: text('token_type'),
scope: text('scope'),
id_token: text('id_token'),
session_state: text('session_state')
},
(account) => [
{
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId]
})
}
]
);
export const sessions = pgTable('session', {
sessionToken: text('sessionToken').primaryKey(),
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expires: timestamp('expires', { mode: 'date' }).notNull()
});
export const verificationTokens = pgTable(
'verificationToken',
{
identifier: text('identifier').notNull(),
token: text('token').notNull(),
expires: timestamp('expires', { mode: 'date' }).notNull()
},
(verificationToken) => [
{
compositePk: primaryKey({
columns: [verificationToken.identifier, verificationToken.token]
})
}
]
);
export const authenticators = pgTable(
'authenticator',
{
credentialID: text('credentialID').notNull().unique(),
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
providerAccountId: text('providerAccountId').notNull(),
credentialPublicKey: text('credentialPublicKey').notNull(),
counter: integer('counter').notNull(),
credentialDeviceType: text('credentialDeviceType').notNull(),
credentialBackedUp: boolean('credentialBackedUp').notNull(),
transports: text('transports')
},
(authenticator) => [
{
compositePK: primaryKey({
columns: [authenticator.userId, authenticator.credentialID]
})
}
]
);

View File

@@ -0,0 +1,3 @@
export * from './auth.ts';
export * from './user.ts';
export * from './akti.ts';

View File

@@ -0,0 +1,11 @@
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('user', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text('name'),
email: text('email').unique(),
emailVerified: timestamp('emailVerified', { mode: 'date' }),
image: text('image')
});

7
src/routes/+error.svelte Normal file
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>

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
src/routes/+layout.svelte Normal file
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>

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
src/routes/+page.svelte Normal file
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>

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;

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import AktiEditor from '$lib/components/akti/AktiEditor.svelte';
</script>
<AktiEditor akti={{ title: '', summary: '', body: '' }} />

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;

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}

5
start.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
pnpm db:migrate
node index.js

3
static/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

18
svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});