feat: auth system

This commit is contained in:
HerozDotExe 2025-08-05 12:39:50 +02:00
parent ed94a8de34
commit 5faf40474f
25 changed files with 778 additions and 9 deletions

View file

@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("_pb_users_auth_")
// update collection data
unmarshal({
"indexes": [
"CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)",
"CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''",
"CREATE UNIQUE INDEX `idx_GloaDPerfv` ON `users` (`name`)"
]
}, collection)
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("_pb_users_auth_")
// update collection data
unmarshal({
"indexes": [
"CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)",
"CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''"
]
}, collection)
return app.save(collection)
})

View file

@ -0,0 +1,42 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("_pb_users_auth_")
// update field
collection.fields.addAt(6, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 255,
"min": 3,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("_pb_users_auth_")
// update field
collection.fields.addAt(6, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 255,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
})

View file

@ -9,7 +9,7 @@
"db_dev": "./db/pocketbase serve --dir ./db/pb_data",
"vite_build": "vite build",
"db_copy": "cp ./db/pocketbase ./out && cp -r ./db/pb_migrations ./out",
"preview": "./out/pocketbase serve --dir ./out/pb_data --publicDir ./out/build",
"preview": "ENV=PROD ./out/pocketbase serve --dir ./out/pb_data --publicDir ./out/build",
"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",
@ -19,16 +19,19 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.536.0",
"@internationalized/date": "^3.8.2",
"@lucide/svelte": "^0.515.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"bits-ui": "^2.9.1",
"clsx": "^2.1.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"pocketbase": "^0.26.2",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
@ -44,7 +47,11 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"esbuild"
]
},
"dependencies": {
"zod": "^4.0.14"
}
}

130
pnpm-lock.yaml generated
View file

@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
zod:
specifier: ^4.0.14
version: 4.0.14
devDependencies:
'@eslint/compat':
specifier: ^1.2.5
@ -14,9 +18,12 @@ importers:
'@eslint/js':
specifier: ^9.18.0
version: 9.32.0
'@internationalized/date':
specifier: ^3.8.2
version: 3.8.2
'@lucide/svelte':
specifier: ^0.536.0
version: 0.536.0(svelte@5.37.3)
specifier: ^0.515.0
version: 0.515.0(svelte@5.37.3)
'@sveltejs/adapter-static':
specifier: ^3.0.8
version: 3.0.8(@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(jiti@2.5.1)(lightningcss@1.30.1)))
@ -29,6 +36,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.1.11(vite@7.0.6(jiti@2.5.1)(lightningcss@1.30.1))
bits-ui:
specifier: ^2.9.1
version: 2.9.1(@internationalized/date@3.8.2)(svelte@5.37.3)
clsx:
specifier: ^2.1.1
version: 2.1.1
@ -44,6 +54,9 @@ importers:
globals:
specifier: ^16.0.0
version: 16.3.0
pocketbase:
specifier: ^0.26.2
version: 0.26.2
prettier:
specifier: ^3.4.2
version: 3.6.2
@ -290,6 +303,15 @@ packages:
resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.3':
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -310,6 +332,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@internationalized/date@3.8.2':
resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@ -327,8 +352,8 @@ packages:
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@lucide/svelte@0.536.0':
resolution: {integrity: sha512-YAeoWU+0B/RriFZZ3wHno1FMkbrVrFdityuo2B0YuphD0vtJWXStzZkWLGVhT3jMb7zhugmhayIg+gI4+AZu1g==}
'@lucide/svelte@0.515.0':
resolution: {integrity: sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==}
peerDependencies:
svelte: ^5
@ -484,6 +509,9 @@ packages:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
@ -673,6 +701,13 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bits-ui@2.9.1:
resolution: {integrity: sha512-Fdtc1TKppfEio6fl0XzkrDG8DSvfbYJdrOh4j5KzaGh0kYA6BskMGIXL4JmsZxUS+O5ESFypNjrQ9z9qs3xAdQ==}
engines: {node: '>=20'}
peerDependencies:
'@internationalized/date': ^3.8.1
svelte: ^5.33.0
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@ -916,6 +951,9 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -1128,6 +1166,9 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pocketbase@0.26.2:
resolution: {integrity: sha512-WA8EOBc3QnSJh8rJ3iYoi9DmmPOMFIgVfAmIGux7wwruUEIzXgvrO4u0W2htfQjGIcyezJkdZOy5Xmh7SxAftw==}
postcss-load-config@3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@ -1263,6 +1304,11 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
runed@0.29.2:
resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
peerDependencies:
svelte: ^5.7.0
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@ -1295,6 +1341,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-to-object@1.0.9:
resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -1316,10 +1365,19 @@ packages:
svelte:
optional: true
svelte-toolbelt@0.9.3:
resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.30.2
svelte@5.37.3:
resolution: {integrity: sha512-7t/ejshehHd+95z3Z7ebS7wsqHDQxi/8nBTuTRwpMgNegfRBfuitCSKTUDKIBOExqfT2+DhQ2VLG8Xn+cBXoaQ==}
engines: {node: '>=18'}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
@ -1361,6 +1419,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.3.6:
resolution: {integrity: sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==}
@ -1458,6 +1519,9 @@ packages:
zimmerframe@1.1.2:
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
zod@4.0.14:
resolution: {integrity: sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==}
snapshots:
'@ampproject/remapping@2.3.0':
@ -1591,6 +1655,17 @@ snapshots:
'@eslint/core': 0.15.1
levn: 0.4.1
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.3':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
'@floating-ui/utils@0.2.10': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -1604,6 +1679,10 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@internationalized/date@3.8.2':
dependencies:
'@swc/helpers': 0.5.17
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
@ -1622,7 +1701,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.4
'@lucide/svelte@0.536.0(svelte@5.37.3)':
'@lucide/svelte@0.515.0(svelte@5.37.3)':
dependencies:
svelte: 5.37.3
@ -1751,6 +1830,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
'@tailwindcss/node@4.1.11':
dependencies:
'@ampproject/remapping': 2.3.0
@ -1946,6 +2029,17 @@ snapshots:
balanced-match@1.0.2: {}
bits-ui@2.9.1(@internationalized/date@3.8.2)(svelte@5.37.3):
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/dom': 1.7.3
'@internationalized/date': 3.8.2
esm-env: 1.2.2
runed: 0.29.2(svelte@5.37.3)
svelte: 5.37.3
svelte-toolbelt: 0.9.3(svelte@5.37.3)
tabbable: 6.2.0
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@ -2211,6 +2305,8 @@ snapshots:
imurmurhash@0.1.4: {}
inline-style-parser@0.2.4: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
@ -2373,6 +2469,8 @@ snapshots:
picomatch@4.0.3: {}
pocketbase@0.26.2: {}
postcss-load-config@3.1.4(postcss@8.5.6):
dependencies:
lilconfig: 2.1.0
@ -2454,6 +2552,11 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
runed@0.29.2(svelte@5.37.3):
dependencies:
esm-env: 1.2.2
svelte: 5.37.3
sade@1.8.1:
dependencies:
mri: 1.2.0
@ -2478,6 +2581,10 @@ snapshots:
strip-json-comments@3.1.1: {}
style-to-object@1.0.9:
dependencies:
inline-style-parser: 0.2.4
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -2505,6 +2612,13 @@ snapshots:
optionalDependencies:
svelte: 5.37.3
svelte-toolbelt@0.9.3(svelte@5.37.3):
dependencies:
clsx: 2.1.1
runed: 0.29.2(svelte@5.37.3)
style-to-object: 1.0.9
svelte: 5.37.3
svelte@5.37.3:
dependencies:
'@ampproject/remapping': 2.3.0
@ -2522,6 +2636,8 @@ snapshots:
magic-string: 0.30.17
zimmerframe: 1.1.2
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {}
tailwind-merge@3.3.1: {}
@ -2559,6 +2675,8 @@ snapshots:
dependencies:
typescript: 5.9.2
tslib@2.8.1: {}
tw-animate-css@1.3.6: {}
type-check@0.4.0:
@ -2614,3 +2732,5 @@ snapshots:
yocto-queue@0.1.0: {}
zimmerframe@1.1.2: {}
zod@4.0.14: {}

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import Input from '$lib/components/ui/input/input.svelte';
import { pb } from '$lib/db.svelte';
import Button from './ui/button/button.svelte';
</script>
<a class="absolute top-[50%] left-4 translate-y-[-50%] text-5xl" href="/">Heddit</a>
<Input
type="search"
placeholder="Search on heddit..."
class="absolute top-[50%] left-[50%] w-[40%] translate-[-50%]"
/>
<div class="absolute top-[50%] right-4 flex w-fit translate-y-[-50%] gap-4">
{#if pb.authStore.record?.name}
<Button
class="cursor-pointer"
onclick={async () => {
pb.authStore.clear();
window.location.reload();
}}>Logout</Button
>
{:else}
<Button variant="secondary" href="/auth/login">Login</Button>
<Button href="/auth/register">Register</Button>
{/if}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View file

@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,51 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
...restProps
}: Props = $props();
</script>
{#if type === "file"}
<input
bind:this={ref}
data-slot="input"
class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot="input"
class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
data-slot="label"
class={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className
)}
{...restProps}
/>

18
src/lib/db.svelte.ts Normal file
View file

@ -0,0 +1,18 @@
import PocketBase from 'pocketbase';
const db_url = import.meta.env.PROD ? '/' : 'http://localhost:8090';
console.log(import.meta.env.PROD, db_url);
export const pb = new PocketBase(db_url);
async function refreshAuth() {
if (pb.authStore.token) {
try {
await pb.collection('users').authRefresh();
} catch (error) {
console.error(error);
}
}
}
refreshAuth();

View file

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

6
src/lib/schemas/login.ts Normal file
View file

@ -0,0 +1,6 @@
import * as z from 'zod';
export const loginSchema = z.object({
email: z.email(),
password: z.string().min(8).max(255)
})

View file

@ -0,0 +1,8 @@
import * as z from 'zod';
export const registerSchema = z.object({
email: z.email(),
name: z.string().min(3).max(255),
password: z.string().min(8).max(255),
passwordConfirm: z.string().min(8).max(255)
})

View file

@ -1,12 +1,19 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import Navigation from '$lib/components/Navigation.svelte';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<title>Heddit</title>
</svelte:head>
{@render children?.()}
<header class="absolute top-0 left-0 w-full h-16">
<Navigation></Navigation>
</header>
<main class="absolute top-16 left-0 w-full h-[calc(100%-4rem)] p-4">
{@render children?.()}
</main>

View file

@ -1,9 +1,11 @@
<script>
import Button from '$lib/components/ui/button/button.svelte';
import { pb } from '$lib/db.svelte';
</script>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <Button variant="link" href="https://svelte.dev/docs/kits">svelte.dev/docs/kit</Button> to read
the documentation
{pb.authStore.record?.name}
</p>

View file

@ -0,0 +1,102 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { pb } from '$lib/db.svelte';
import { goto } from '$app/navigation';
import { loginSchema } from '$lib/schemas/login';
import { ZodError } from 'zod';
import { ClientResponseError } from 'pocketbase';
let error = '';
const form = {
email: '',
password: ''
};
interface FormErrors {
email: boolean;
password: boolean;
[key: string]: boolean;
}
const formErrors = {
email: false,
password: false
} as FormErrors;
function validateForm() {
try {
loginSchema.parse(form);
return true;
} catch (e) {
if (e instanceof ZodError) {
error = e.issues[0].message;
formErrors[e.issues[0].path[0] as string] = true;
return false;
}
}
}
function handleDBErrors(e: ClientResponseError) {
const element = Object.keys(e.data.data)[0];
formErrors[element] = true;
const dbError = e.data.data[element];
error = dbError.message;
}
async function login() {
// reset all errors
error = '';
Object.keys(formErrors).forEach((i) => {
formErrors[i] = false;
});
if (validateForm()) {
try {
await pb.collection('users').authWithPassword(form.email, form.password);
await goto('/');
window.location.reload();
} catch (e) {
if (e instanceof ClientResponseError) {
handleDBErrors(e);
}
}
}
}
</script>
<Card.Root class="absolute top-[50%] left-[50%] w-full max-w-sm translate-[-50%]">
<Card.Header>
<Card.Title>Login to your account</Card.Title>
<Card.Description>Enter your email below to login</Card.Description>
<Card.Action>
<Button variant="link" href="/auth/register">Register</Button>
</Card.Action>
</Card.Header>
<Card.Content>
<form>
<div class="flex flex-col gap-6">
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required />
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<a href="##" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">
Forgot your password?
</a>
</div>
<Input id="password" type="password" required />
</div>
</div>
</form>
</Card.Content>
<Card.Footer class="flex-col gap-2">
<Button type="submit" class="w-full cursor-pointer" onclick={login}>Login</Button>
<p class="mt-4 text-red-700">{error}</p>
</Card.Footer>
</Card.Root>

View file

@ -0,0 +1,151 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { pb } from '$lib/db.svelte';
import { goto } from '$app/navigation';
import { registerSchema } from '$lib/schemas/register';
import { ZodError } from 'zod';
import { ClientResponseError } from 'pocketbase';
let error = '';
const form = {
email: '',
name: '',
password: '',
passwordConfirm: ''
};
interface FormErrors {
email: boolean;
name: boolean;
password: boolean;
passwordConfirm: boolean;
[key: string]: boolean;
}
const formErrors = {
email: false,
name: false,
password: false,
passwordConfirm: false
} as FormErrors;
function validateForm() {
try {
registerSchema.parse(form);
if (form.password !== form.passwordConfirm) {
formErrors.password = true;
formErrors.passwordConfirm = true;
error = "Passwords don't match";
return false;
}
return true;
} catch (e) {
if (e instanceof ZodError) {
error = e.issues[0].message;
formErrors[e.issues[0].path[0] as string] = true;
return false;
}
}
}
function handleDBErrors(e: ClientResponseError) {
const element = Object.keys(e.data.data)[0];
formErrors[element] = true;
const dbError = e.data.data[element];
if (dbError.code === 'validation_not_unique') {
error = `This ${element} is already used by someone. Maybe try logging in if it's you.`;
} else error = dbError.message;
}
async function register() {
// reset all errors
error = '';
Object.keys(formErrors).forEach((i) => {
formErrors[i] = false;
});
if (validateForm()) {
try {
await pb.collection('users').create(form);
await pb.collection('users').authWithPassword(form.email, form.password);
await goto('/');
window.location.reload();
} catch (e) {
if (e instanceof ClientResponseError) {
handleDBErrors(e);
}
}
}
}
</script>
<Card.Root class="absolute top-[50%] left-[50%] w-full max-w-sm translate-[-50%]">
<Card.Header>
<Card.Title>Register to heddit</Card.Title>
<Card.Description>Enter your email below to register</Card.Description>
<Card.Action>
<Button variant="link" href="/auth/login">Login</Button>
</Card.Action>
</Card.Header>
<Card.Content>
<form>
<div class="flex flex-col gap-6">
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
aria-invalid={formErrors.email}
id="email"
type="email"
placeholder="someone@example.com"
required
bind:value={form.email}
/>
</div>
<div class="grid gap-2">
<Label for="name">Username</Label>
<Input
aria-invalid={formErrors.name}
id="name"
type="text"
placeholder="Someone"
required
bind:value={form.name}
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
</div>
<Input
aria-invalid={formErrors.password}
id="password"
type="password"
required
bind:value={form.password}
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="passwordConfirm">Confirm your password</Label>
</div>
<Input
aria-invalid={formErrors.passwordConfirm}
id="passwordConfirm"
type="password"
required
bind:value={form.passwordConfirm}
/>
</div>
</div>
</form>
</Card.Content>
<Card.Footer class="flex-col gap-2">
<Button type="submit" class="w-full cursor-pointer" onclick={register}>Register</Button>
<p class="mt-4 text-red-700">{error}</p>
</Card.Footer>
</Card.Root>