initial commit

This commit is contained in:
Ludwig Lehnert 2025-06-13 00:23:19 +02:00
commit e77adbfc9f
Signed by: ludwig
SSH Key Fingerprint: SHA256:4vshH9GJ8TLO1RS2fY6rDDLnq7+KVvSClCY+uEhYYRA
32 changed files with 4609 additions and 0 deletions

4
.env.example Normal file
View File

@ -0,0 +1,4 @@
S3_HOST=...
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=...

25
.gitignore vendored Normal file
View File

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

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

0
Dockerfile Normal file
View File

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# 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 `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
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:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

3766
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "dailies",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/pako": "^2.0.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
},
"dependencies": {
"@noble/ed25519": "^2.3.0",
"@noble/hashes": "^1.8.0",
"@tailwindcss/vite": "^4.1.10",
"pako": "^2.1.0",
"tailwindcss": "^4.1.10",
"@aws-sdk/client-s3": "^3.828.0"
}
}

64
src/app.css Normal file
View File

@ -0,0 +1,64 @@
@import "tailwindcss";
html,
body {
height: 100%;
scroll-behavior: smooth;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes scale-in {
0% {
scale: 0;
}
100% {
scale: 1;
}
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-in {
opacity: 0;
animation-name: fade-in;
animation-fill-mode: forwards;
animation-timing-function: linear;
animation-duration: 350ms;
}
.scale-in {
scale: 0;
animation-name: scale-in;
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(.17,.67,.41,1.3);
animation-duration: 350ms;
}
.blink {
animation-name: blink;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-duration: 1s;
}

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 {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div id="root" style="display: contents">%sveltekit.body%</div>
</body>
</html>

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

@ -0,0 +1,26 @@
import { decodeKeyVault, deriveKeyVault, encodeKeyVault, type KeyVault } from "./crypto";
export function loggedIn() {
if (typeof window === 'undefined') return false;
return !!sessionStorage['_vault'];
}
export async function login(passkey: string): Promise<KeyVault> {
const keyVault = await deriveKeyVault(passkey);
sessionStorage['_vault'] = encodeKeyVault(keyVault);
return keyVault;
}
export function getKeyVault(): KeyVault | null {
try {
return decodeKeyVault(sessionStorage['_vault']);
} catch (e) {
console.log(e);
return null;
}
}
export function logout() {
delete sessionStorage['_vault'];
}

125
src/lib/crypto.ts Normal file
View File

@ -0,0 +1,125 @@
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2';
import { gzip, ungzip } from 'pako';
ed.etc.sha512Sync = sha512;
const SALT = 'SONRI-KEY-SALT';
export type KeyVault = Awaited<ReturnType<typeof deriveKeyVault>>;
// Helper: convert string to Uint8Array
function strToUint8(str: string) {
return new TextEncoder().encode(str);
}
function uint8ToStr(uint8: Uint8Array) {
return new TextDecoder().decode(uint8);
}
export function uint8ToHex(uint8: Uint8Array) {
return Array.from(uint8)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
export function hexToUint8(hex: string): Uint8Array {
if (hex.length % 2 !== 0) {
throw new Error('Hex string must have an even length');
}
const length = hex.length / 2;
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return uint8;
}
function uint8ToBase64(uint8: Uint8Array) {
return atob(uint8ToStr(uint8));
}
function base64ToUint8(base64: string) {
return strToUint8(btoa(base64));
}
export async function deriveKeyVault(secret: string) {
let hash = strToUint8(secret), last = hash;
for (let i = 0; i < 10000; i++) {
const hashInput = Uint8Array.from([...strToUint8(SALT), ...hash]);
const hashBuffer = await crypto.subtle.digest('SHA-512', hashInput);
last = hash;
hash = new Uint8Array(hashBuffer);
}
const symmetricKey = last.slice(0, 32);
const privateKey = hash.slice(0, 32);
const publicKey = ed.getPublicKey(privateKey);
return {
publicKey,
privateKey,
symmetricKey,
};
}
export function encodeKeyVault(keyVault: KeyVault) {
const jsonStr = JSON.stringify(Object.fromEntries(Object.entries(keyVault).map(([key, value]) => {
return [key, uint8ToHex(value)];
})));
return uint8ToHex(gzip(strToUint8(jsonStr)));
}
export function decodeKeyVault(encoded: string) {
const jsonStr = uint8ToStr(ungzip(hexToUint8(encoded)));
return Object.fromEntries(Object.entries(JSON.parse(jsonStr)).map(([key, value]) => {
return [key, hexToUint8(value as string)];
})) as KeyVault;
}
export async function encrypt(vault: KeyVault, data: Uint8Array) {
const key = await crypto.subtle.importKey(
'raw',
vault.symmetricKey,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt'],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
return Uint8Array.from([...iv, ...new Uint8Array(encryptedBuffer)]);
}
export async function decrypt(vault: KeyVault, cipher: Uint8Array) {
const key = await crypto.subtle.importKey(
'raw',
vault.symmetricKey,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt'],
);
const iv = cipher.slice(0, 12);
cipher = cipher.slice(12);
const dataBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
cipher
);
return new Uint8Array(dataBuffer);
}

4
src/lib/date.ts Normal file
View File

@ -0,0 +1,4 @@
export function dateDiff(a: Date, b: Date) {
const diff = (a.getTime() - b.getTime()) - 60 * 60 * 1000;
return new Date(diff);
}

36
src/lib/ffmpeg.server.ts Normal file
View File

@ -0,0 +1,36 @@
import * as path from 'path';
import * as fs from 'fs/promises';
import { exec } from 'child_process';
import { hashBuffer } from './hash';
export async function convertVideo(video: Uint8Array) {
let random = Math.floor(Math.random() * 10000000);
let hash = await hashBuffer(video.buffer);
const tempInPath = `/tmp/${hash}-${random}.in.webm`;
const tempOutPath = `/tmp/${hash}-${random}.out.webm`;
try {
await fs.writeFile(tempInPath, video);
const status = await new Promise((resolve, reject) => {
const child = exec(`ffmpeg -i "${tempInPath}" -preset ultrafast -vf "scale=320:480,eq=saturation=1.4,unsharp=5:5:1.5:5:5:0.0" -b:v 420k -b:a 64k "${tempOutPath}"`, (err) => {
if (err) {
reject(err);
return;
}
resolve(child.exitCode);
});
});
if (status) return null;
const result = await fs.readFile(tempOutPath);
const resultUint8 = new Uint8Array(result);
return resultUint8;
} finally {
await fs.unlink(tempInPath).catch((_) => {});
await fs.unlink(tempOutPath).catch((_) => {});
}
}

5
src/lib/hash.ts Normal file
View File

@ -0,0 +1,5 @@
export async function hashBuffer(buffer: ArrayBufferLike) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer as any);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 10);
}

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.

12
src/lib/storage.server.ts Normal file
View File

@ -0,0 +1,12 @@
import { S3_HOST, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET } from '$env/static/private';
import { S3Client } from '@aws-sdk/client-s3';
export const s3 = new S3Client({
region: 'eu-central',
endpoint: `https://${S3_HOST}`,
forcePathStyle: true, // Required for many custom S3 endpoints
credentials: {
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET_KEY
}
});

View File

@ -0,0 +1,3 @@
<div class="min-h-full bg-[#e7f7df]">
<slot />
</div>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { getKeyVault, loggedIn } from "$lib/auth";
import { decrypt, uint8ToHex } from "$lib/crypto";
import { onMount } from "svelte";
let entryUrls = $state<any[] | null>(null);
let videoSrc = $state<string | null>(null);
onMount(() => {
if (!window) return;
if (!loggedIn()) {
window.location.href = '/login';
return;
}
const vault = getKeyVault();
fetch(`/api/log?pubkey=${encodeURIComponent(uint8ToHex(vault!.publicKey))}`).then(async res => {
if (!res.ok) return;
entryUrls = (await res.json() as any[]).sort((a, b) => b.date.localeCompare(a.date));
console.log(entryUrls);
});
});
const update = async (date: string) => {
videoSrc = null;
const entry = entryUrls?.find((e) => e.date === date);
if (!entry) return;
const res = await fetch(entry.url);
if (!res.ok) return;
const cipherBuffer = await res.bytes();
console.log(cipherBuffer);
const vault = getKeyVault()!;
const videoData = await decrypt(vault, new Uint8Array(cipherBuffer));
const blob = new Blob([videoData], { type: 'video/webm' });
videoSrc = URL.createObjectURL(blob);
};
</script>
<div class="h-screen p-4 flex flex-col items-center">
<div class="shrink-0">
<input type="date"
class="px-4 py-2 rounded-full bg-white min-w-0"
onchange={(e) => update((e.target as any).value)}>
</div>
<div class="grow w-full pt-3 flex flex-col items-center justify-items-stretch overflow-hidden">
<video src={videoSrc} controls
class="h-full w-full min-w-0 min-h-0 max-w-[70vh] object-cover rounded-[40px] bg-[#303030]">
<track kind="captions" />
</video>
</div>
</div>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { loggedIn, login } from "$lib/auth";
import { deriveKeyVault, encodeKeyVault } from "$lib/crypto";
import { onMount } from "svelte";
const onSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const formData = new FormData(e.target as any);
const passkey = formData.get('passkey') as string;
await login(passkey);
window.location.href = '/today';
};
onMount(() => {
if (!window) return;
if (loggedIn()) {
window.location.href = '/today';
}
});
</script>
<div class="h-full grid place-items-center p-6">
<form method="POST" class="p-6 rounded-3xl bg-white flex flex-col gap-2"
onsubmit={onSubmit}
>
<h1 class="text-2xl font-bold">Login</h1>
<input type="password" name="passkey" placeholder="Passkey"
class="px-3 py-1 rounded-3xl outline-none bg-[#00000010]">
<button type="submit" class="px-3 py-1 rounded-3xl cursor-pointer bg-[#2596be] cusor-pointer text-white">
Login
</button>
</form>
</div>

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { loggedIn } from "$lib/auth";
import { onMount } from "svelte";
onMount(() => {
if (!window) return;
if (!loggedIn()) {
window.location.href = '/login';
}
});
</script>

View File

@ -0,0 +1,7 @@
import type { Actions } from "./$types";
export const actions: Actions = {
default: async (event) => {
},
};

View File

@ -0,0 +1,185 @@
<script lang="ts">
import { onMount } from "svelte";
import { hashBuffer } from "$lib/hash";
import { dateDiff } from "$lib/date";
import { getKeyVault, loggedIn } from "$lib/auth";
import { encrypt, uint8ToHex } from "$lib/crypto";
type StatusT = "pending" | "active" | "error";
let status = $state<StatusT>("pending");
let errors = $state<string[]>([]);
let now = $state(new Date());
let recordingStart = $state<Date | null>(null);
let recordingURL = $state<string | null>(null);
let video: HTMLVideoElement, finalVideo: HTMLVideoElement;
let recorder: MediaRecorder | null = null;
onMount(() => {
if (!window) return;
if (!loggedIn()) {
window.location.href = '/login';
}
});
onMount(() => {
if (!window) return;
const interval = setInterval(() => {
now = new Date();
}, 100);
return () => clearInterval(interval);
});
onMount(async () => {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
return;
}
const WIDTH = 320;
const HEIGHT = 480;
const FPS = 24;
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: WIDTH, height: HEIGHT, frameRate: FPS, facingMode: 'user' },
audio: true,
});
video.srcObject = stream;
recorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs="vp8"' });
recorder.addEventListener('dataavailable', async (e) => {
const inputBlob = new Blob([e.data], { type: 'video/webm' });
const convertRes = await fetch('/api/convert', {
method: 'POST',
body: inputBlob,
});
if (!convertRes.ok) return;
const convertBlob = await convertRes.blob();
recordingURL = URL.createObjectURL(convertBlob);
setTimeout(() => finalVideo.scrollIntoView(), 150);
})
});
const start = () => {
recordingStart = new Date();
recorder?.start();
};
const stop = () => {
recordingStart = null;
recorder?.stop();
};
const submit = async () => {
if (!recordingURL) return;
const blob = await (await fetch(recordingURL)).blob();
const buffer = await blob.arrayBuffer();
console.log('buffer', buffer);
const vault = getKeyVault()!;
const encrypted = await encrypt(vault, new Uint8Array(buffer));
console.log('encrypted', encrypted);
const res = await fetch(`/api/today?pubkey=${uint8ToHex(vault.publicKey)}`, {
method: 'POST',
body: encrypted,
headers: {
'Content-Type': 'application/octet-stream',
},
});
if (res.ok) {
window.location.href = '/log';
}
};
</script>
<div class="min-h-screen">
<div class="h-screen p-4 min-h-0 grid place-items-center">
<video
autoplay muted bind:this={video}
class="opacity-0 h-full w-full min-h-0 max-w-[70vh] object-cover rounded-[40px] bg-[#303030] fade-in"
style="animation-delay: 500ms;"
>
<track kind="captions" />
</video>
<div
class="absolute top-0 left-0 right-0 p-6 m-auto w-fit scale-in"
style="animation-delay: 50ms;"
>
<div class="rounded-3xl bg-white px-3 py-1 font-serif text-center w-fit">
<h1 class="font-bold text-xl">{now.toLocaleDateString(navigator.language, { dateStyle: 'long' })}</h1>
<h2 class="text-sm">{now.toLocaleTimeString(navigator.language)}</h2>
</div>
{#if !!recordingStart}
<div class="h-1"></div>
<div class="rounded-3xl bg-white px-3 py-1 font-serif flex gap-1 items-center w-fit m-auto scale-in">
<svg class="blink" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="24px" fill="red"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
<div>
<span class="text-md">
<!-- <span class="font-bold">Recording</span> -->
<span>{dateDiff(now, recordingStart).toLocaleTimeString(navigator.language, { minute: 'numeric', second: 'numeric' })}</span>
</span>
</div>
</div>
{/if}
</div>
<div
class="absolute bottom-0 left-0 right-0 p-6 m-auto w-fit fade-in"
style="animation-delay: 50ms;"
>
<button
class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer ${recordingStart ? 'bg-[#e14c2f]' : 'bg-[#2596be]'}`}
style="transition-duration: 250ms;"
onclick={() => recordingStart ? stop() : start()}
>
{recordingStart ? 'Stop' : 'Start'}
</button>
</div>
</div>
{#if recordingURL}
<div class="h-8"></div>
<div class="p-4 min-h-0 grid place-items-center">
<video controls bind:this={finalVideo}
src={recordingURL}
class="opacity-0 h-full w-full max-w-[50vh] min-h-0 w-full object-cover rounded-[40px] bg-[#303030] fade-in"
style="aspect-ratio: 320 / 480;"
>
<track kind="captions" />
</video>
<div class="h-2"></div>
<button
class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer bg-[#2596be]`}
style="transition-duration: 250ms;"
onclick={submit}
>
Submit
</button>
</div>
{/if}
</div>
<!-- <canvas class="hidden" bind:this={renderCanvas}></canvas>
<canvas class="hidden" bind:this={captureCanvas}></canvas>
<video class="hidden" bind:this={sourceVideo}>
<track kind="captions" />
</video> -->
<style>
</style>

View File

@ -0,0 +1,5 @@
<script>
import "../app.css";
</script>
<slot />

17
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,17 @@
<script lang="ts">
import { loggedIn } from "$lib/auth";
import { onMount } from "svelte";
onMount(() => {
if (!window) return;
console.log('test', loggedIn());
if (loggedIn()) {
window.location.href = '/today';
}
});
</script>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View File

@ -0,0 +1,17 @@
import { convertVideo } from "$lib/ffmpeg.server";
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {
const bodyBuffer = await request.arrayBuffer();
const bodyUint8 = new Uint8Array(bodyBuffer);
const convertedUint8 = await convertVideo(bodyUint8);
if (!convertedUint8) error(500);
return new Response(convertedUint8, {
headers: {
'Content-Type': 'video/webm',
},
});
}

View File

@ -0,0 +1,34 @@
import { s3 } from '$lib/storage.server';
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { ListObjectsV2Command } from '@aws-sdk/client-s3';
import { S3_BUCKET, S3_HOST } from '$env/static/private';
export const GET: RequestHandler = async ({ url }) => {
const pubkeyHex = url.searchParams.get('pubkey')?.trim()?.toLowerCase();
if (!pubkeyHex || !/^[0-9a-f]{64}$/.test(pubkeyHex)) error(400);
const objects = [];
let continuationToken: string | undefined;
do {
const command = new ListObjectsV2Command({
Bucket: S3_BUCKET,
Prefix: `${pubkeyHex}/`,
ContinuationToken: continuationToken,
});
const res = await s3.send(command);
objects.push(...(res.Contents ?? []));
continuationToken = res.NextContinuationToken;
} while (continuationToken);
return json(objects.filter(obj => !!obj.Key).map(obj => {
return {
date: obj.Key!.split('/').at(-1),
url: `https://${S3_BUCKET}.${S3_HOST}/${encodeURIComponent(obj.Key!)}`
};
}));
}

View File

@ -0,0 +1,29 @@
import { convertVideo } from "$lib/ffmpeg.server";
import { error, json, text } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { S3_BUCKET } from "$env/static/private";
import { s3 } from "$lib/storage.server";
export const POST: RequestHandler = async ({ request, url }) => {
const pubkeyHex = url.searchParams.get('pubkey')?.trim()?.toLowerCase();
if (!pubkeyHex || !/^[0-9a-f]{64}$/.test(pubkeyHex)) error(400);
const today = new Date().toISOString().slice(0, 10);
const bodyBuffer = await request.arrayBuffer();
const bodyUint8 = new Uint8Array(bodyBuffer);
console.log(today, bodyUint8.byteLength);
const command = new PutObjectCommand({
Bucket: S3_BUCKET,
Key: `${pubkeyHex}/${today}`,
Body: bodyUint8,
ContentType: 'application/octet-stream',
});
await s3.send(command);
return text('ok');
}

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
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;

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"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
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

7
vite.config.ts Normal file
View File

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