register / login
This commit is contained in:
21
bun.lock
21
bun.lock
@@ -7,23 +7,24 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mode-watcher": "0.5.1",
|
||||
"mode-watcher": "^1.0.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.5.6",
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.513.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"bits-ui": "^2.4.1",
|
||||
"bits-ui": "^2.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.4",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
@@ -208,7 +209,7 @@
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"bits-ui": ["bits-ui@2.4.1", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/dom": "^1.7.0", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.28.0", "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-Z0qZkPgtxP5dkEOyZqCK2PQ1oFXsD0AlfFMtsrMLJqaYtjUBoQGosHVKEH03M9q4G8kxVnG74Zb0F9pS0X4+6w=="],
|
||||
"bits-ui": ["bits-ui@2.5.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/dom": "^1.7.0", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.28.0", "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-PbjylA1UWd4A/c5AYqie/EVxQ1/8uugmJKLg9whLoBBHbfPEBGhK09dCPrahK9kA6DRHhMmij0XXIUGIfrmNow=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
@@ -312,7 +313,7 @@
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mode-watcher": ["mode-watcher@0.5.1", "", { "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.1" } }, "sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w=="],
|
||||
"mode-watcher": ["mode-watcher@1.0.7", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
@@ -358,6 +359,8 @@
|
||||
|
||||
"svelte-check": ["svelte-check@4.2.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA=="],
|
||||
|
||||
"svelte-sonner": ["svelte-sonner@1.0.4", "", { "dependencies": { "runed": "^0.26.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-ctm9jeV0Rf3im2J6RU1emccrJFjRSdNSPsLlxaF62TLZw9bB1D40U/U7+wqEgohJY/X7FBdghdj0BFQF/IqKPQ=="],
|
||||
|
||||
"svelte-toolbelt": ["svelte-toolbelt@0.9.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.28.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
@@ -404,6 +407,14 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
|
||||
|
||||
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||
|
||||
"svelte-sonner/runed": ["runed@0.26.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-qWFv0cvLVRd8pdl/AslqzvtQyEn5KaIugEernwg9G98uJVSZcs/ygvPBvF80LA46V8pwRvSKnaVLDI3+i2wubw=="],
|
||||
|
||||
"tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
|
||||
|
||||
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
@@ -12,5 +12,5 @@
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
@@ -14,20 +14,21 @@
|
||||
"lint": "prettier --check ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.5.6",
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.513.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"bits-ui": "^2.4.1",
|
||||
"bits-ui": "^2.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.4",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
@@ -39,6 +40,6 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mode-watcher": "0.5.1"
|
||||
"mode-watcher": "^1.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
20
src/lib/Navbar.svelte
Normal file
20
src/lib/Navbar.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { LogOutIcon } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
<nav class="flex h-12 w-full items-center justify-center border border-b">
|
||||
<div class="flex w-full max-w-screen-2xl flex-row">
|
||||
<div class="flex-grow"></div>
|
||||
<form
|
||||
class={['flex flex-shrink flex-row-reverse justify-end', page.data.token == null && 'hidden']}
|
||||
method="POST"
|
||||
action="/?/logout"
|
||||
use:enhance
|
||||
>
|
||||
<Button type="submit" variant="outline" size="icon"><LogOutIcon /></Button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1 +1,9 @@
|
||||
import { decode, type JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
export const AUTH_COOIKIE_NAME: string = 'auth-token' as const;
|
||||
|
||||
export function getUserIdFromToken(token: string): number {
|
||||
const decoded = decode(token) as JwtPayload;
|
||||
|
||||
return decoded.userId;
|
||||
}
|
||||
|
||||
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
src/lib/components/ui/label/label.svelte
Normal file
20
src/lib/components/ui/label/label.svelte
Normal 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}
|
||||
/>
|
||||
1
src/lib/components/ui/sonner/index.ts
Normal file
1
src/lib/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
13
src/lib/components/ui/sonner/sonner.svelte
Normal file
13
src/lib/components/ui/sonner/sonner.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--popover); --normal-text: var(--popover-foreground); --normal-border: var(--border);"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -1,43 +1,22 @@
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
import { AUTH_COOIKIE_NAME } from '$lib/auth';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { AUTH_COOIKIE_NAME, getUserIdFromToken } from '$lib/auth';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { decode, type JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ fetch, cookies }) => {
|
||||
export const load: LayoutServerLoad = async ({ cookies, url }) => {
|
||||
let token = cookies.get(AUTH_COOIKIE_NAME);
|
||||
|
||||
if (token != null) {
|
||||
const userId = getUserIdFromToken(token);
|
||||
if (token == null) {
|
||||
if (url.pathname !== '/register' && url.pathname !== '/login') {
|
||||
return redirect(303, '/login');
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userId
|
||||
};
|
||||
return { };
|
||||
}
|
||||
|
||||
const response = await fetch(`${PUBLIC_BACKEND_URL}/auth`);
|
||||
|
||||
if (!response.ok) {
|
||||
return error(response.status);
|
||||
}
|
||||
|
||||
token = await response.text();
|
||||
const userId = getUserIdFromToken(token);
|
||||
|
||||
cookies.set(AUTH_COOIKIE_NAME, token, {
|
||||
path: '/',
|
||||
secure: true
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
userId
|
||||
};
|
||||
};
|
||||
|
||||
function getUserIdFromToken(token: string): number {
|
||||
const decoded = decode(token) as JwtPayload;
|
||||
|
||||
return decoded.userId;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { setMessageStore } from '$lib/message.svelte';
|
||||
import type { LayoutServerData } from './$types.js';
|
||||
import Navbar from '$lib/Navbar.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
@@ -26,8 +28,10 @@
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center">
|
||||
<Navbar/>
|
||||
<div class="my-8 flex h-full w-full max-w-screen-2xl flex-col">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { error, fail, type Actions } from '@sveltejs/kit';
|
||||
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
import { AUTH_COOIKIE_NAME } from '$lib/auth';
|
||||
import type { PageServerLoad } from './$types';
|
||||
@@ -47,5 +47,10 @@ export const actions = {
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
},
|
||||
logout: async ({ cookies }) => {
|
||||
cookies.delete(AUTH_COOIKIE_NAME, { path: '/' });
|
||||
|
||||
redirect(303, '/login');
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
29
src/routes/login/+page.server.ts
Normal file
29
src/routes/login/+page.server.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PUBLIC_BACKEND_URL } from "$env/static/public";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import type { Actions } from "./$types";
|
||||
import { AUTH_COOIKIE_NAME } from "$lib/auth";
|
||||
|
||||
export const actions = {
|
||||
login: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const response = await fetch(`${PUBLIC_BACKEND_URL}/login`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return fail(response.status, { message: 'Username or password are incorrect' });
|
||||
}
|
||||
|
||||
const token = await response.text();
|
||||
|
||||
cookies.set(AUTH_COOIKIE_NAME, token, {
|
||||
path: '/',
|
||||
secure: true
|
||||
});
|
||||
|
||||
return redirect(303, '/');
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
76
src/routes/login/+page.svelte
Normal file
76
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { SubmitFunction } from './$types';
|
||||
|
||||
const submitLogin: SubmitFunction = async ({ formData }) => {
|
||||
const username = formData.get('name');
|
||||
|
||||
return async ({ update, result }) => {
|
||||
await update({
|
||||
invalidateAll: true,
|
||||
reset: true
|
||||
});
|
||||
|
||||
if (result.type === 'redirect') {
|
||||
toast.success('Login', {
|
||||
description: `Successfully logged in as ${username}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'failure') {
|
||||
toast.error('Login', {
|
||||
description: result.data?.message ?? 'Failed to login'
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="mt-16 flex w-fit flex-col justify-center space-y-4 self-center"
|
||||
method="POST"
|
||||
action="?/login"
|
||||
use:enhance={submitLogin}
|
||||
>
|
||||
<h1 class="scroll-m-20 text-xl font-semibold tracking-tight">Login</h1>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="name">Username</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="w-fit min-w-48"
|
||||
placeholder="cool-name"
|
||||
minlength={2}
|
||||
maxlength={24}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="w-fit min-w-48"
|
||||
placeholder="mysecretpassword123"
|
||||
minlength={8}
|
||||
maxlength={64}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a class="text-muted-foreground text-sm" href="/register">Register instead?</a>
|
||||
<Button type="submit">Login</Button>
|
||||
</div>
|
||||
</form>
|
||||
29
src/routes/register/+page.server.ts
Normal file
29
src/routes/register/+page.server.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PUBLIC_BACKEND_URL } from "$env/static/public";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import type { Actions } from "./$types";
|
||||
import { AUTH_COOIKIE_NAME } from "$lib/auth";
|
||||
|
||||
export const actions = {
|
||||
register: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const response = await fetch(`${PUBLIC_BACKEND_URL}/register`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return fail(response.status);
|
||||
}
|
||||
|
||||
const token = await response.text();
|
||||
|
||||
cookies.set(AUTH_COOIKIE_NAME, token, {
|
||||
path: '/',
|
||||
secure: true
|
||||
});
|
||||
|
||||
return redirect(303, '/');
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
50
src/routes/register/+page.svelte
Normal file
50
src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="mt-16 flex w-fit flex-col justify-center space-y-4 self-center"
|
||||
method="POST"
|
||||
action="?/register"
|
||||
use:enhance
|
||||
>
|
||||
<h1 class="scroll-m-20 text-xl font-semibold tracking-tight">Register</h1>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="name">Username</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="w-fit min-w-48"
|
||||
placeholder="cool-name"
|
||||
minlength={2}
|
||||
maxlength={24}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="w-fit min-w-48"
|
||||
placeholder="mysecretpassword123"
|
||||
minlength={8}
|
||||
maxlength={64}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a class="text-muted-foreground text-sm" href="/login">Login instead?</a>
|
||||
<Button type="submit">Register</Button>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user