aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2026-02-22 13:02:35 +0200
committerrtkay123 <dev@kanjala.com>2026-02-22 13:02:35 +0200
commit3fef60a3daf7d17dff22d815400e03f36e4128c9 (patch)
treef806ab0df97fd314b8679eb483a8af9c8d344bfa
parent386ea2dc8271de95ac63864300f7198bdd445e23 (diff)
downloadsellershut-3fef60a3daf7d17dff22d815400e03f36e4128c9.tar.bz2
sellershut-3fef60a3daf7d17dff22d815400e03f36e4128c9.zip
feat(web): oauth redirect
-rw-r--r--website/package.json3
-rw-r--r--website/pnpm-lock.yaml8
-rw-r--r--website/src/lib/schemas/profile.ts13
-rw-r--r--website/src/routes/+layout.svelte4
-rw-r--r--website/src/routes/+page.svelte4
-rw-r--r--website/src/routes/login/+page.svelte94
-rw-r--r--website/src/routes/welcome/+page.server.ts38
-rw-r--r--website/src/routes/welcome/+page.svelte219
8 files changed, 338 insertions, 45 deletions
diff --git a/website/package.json b/website/package.json
index ed45685..7f30897 100644
--- a/website/package.json
+++ b/website/package.json
@@ -40,6 +40,7 @@
"vitest": "^4.0.18"
},
"dependencies": {
- "lucide-svelte": "^0.564.0"
+ "lucide-svelte": "^0.564.0",
+ "zod": "^4.3.6"
}
}
diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml
index 1e9ef82..e7fb2e1 100644
--- a/website/pnpm-lock.yaml
+++ b/website/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
lucide-svelte:
specifier: ^0.564.0
version: 0.564.0(svelte@5.51.0)
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
devDependencies:
'@eslint/compat':
specifier: ^2.0.2
@@ -1614,6 +1617,9 @@ packages:
zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
snapshots:
'@esbuild/aix-ppc64@0.27.3':
@@ -2870,3 +2876,5 @@ snapshots:
yocto-queue@0.1.0: {}
zimmerframe@1.1.4: {}
+
+ zod@4.3.6: {}
diff --git a/website/src/lib/schemas/profile.ts b/website/src/lib/schemas/profile.ts
new file mode 100644
index 0000000..7272800
--- /dev/null
+++ b/website/src/lib/schemas/profile.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod';
+
+export const profileSchema = z.object({
+ username: z
+ .string()
+ .min(3, { message: 'Username must be at least 3 characters' })
+ .max(20, { message: 'Username is too long' })
+ .regex(/^[a-zA-Z0-9_]+$/, { message: 'Only letters, numbers, and underscores allowed' }),
+ bio: z.string().max(160, { message: 'Bio must be under 160 characters' }).optional().default(''),
+});
+
+// Automatically generate a TS type from the Zod schema
+export type ProfileSchema = z.infer<typeof profileSchema>;
diff --git a/website/src/routes/+layout.svelte b/website/src/routes/+layout.svelte
index e68c098..48f0b78 100644
--- a/website/src/routes/+layout.svelte
+++ b/website/src/routes/+layout.svelte
@@ -12,5 +12,7 @@
<div class="flex min-h-screen w-screen">
<NavigationBar />
- {@render children()}
+ <div class="flex w-full items-center justify-center">
+ {@render children()}
+ </div>
</div>
diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte
index cc88df0..a9c8dd0 100644
--- a/website/src/routes/+page.svelte
+++ b/website/src/routes/+page.svelte
@@ -1,2 +1,6 @@
+<script lang="ts">
+ import { profileSchema } from "$lib/schemas/profile";
+
+</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>
diff --git a/website/src/routes/login/+page.svelte b/website/src/routes/login/+page.svelte
index 29be51e..1226799 100644
--- a/website/src/routes/login/+page.svelte
+++ b/website/src/routes/login/+page.svelte
@@ -1,51 +1,59 @@
-<script>
- import Logo from "$lib/components/logo.svelte";
+<script lang="ts">
+ import Logo from '$lib/components/logo.svelte';
+ import { PUBLIC_BACKEND_URL } from '$env/static/public';
+
+ const url = (provider: string): string => {
+ const url = new URL('/auth', PUBLIC_BACKEND_URL);
+ url.searchParams.set('provider', provider);
+ return url.toString();
+ };
</script>
-<div class="flex w-full items-center justify-center">
- <div
- class="w-full max-w-md transform-gpu space-y-8 rounded-2xl border border-gray-100 bg-white p-10 shadow-xl transition-all"
- >
- <div class="text-center">
- <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full">
- <Logo class="h-full w-full" />
- </div>
- <h2 class="mt-6 text-3xl font-extrabold text-gray-900">sellershut</h2>
- <p class="mt-2 text-sm text-gray-600">Please sign in to access the platform</p>
- </div>
- <div class="mt-8 space-y-4">
- <button
- class="group relative flex w-full transform-gpu items-center justify-center rounded-xl border border-gray-900 bg-white px-4 py-3 text-sm font-medium text-gray-900 transition-all duration-200 hover:bg-gray-50 focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 focus:outline-none active:scale-[0.98]"
- >
- <span class="absolute inset-y-0 left-0 flex items-center pl-4">
- <svg
- class="h-5 w-5 text-gray-400 transition-colors duration-200 group-hover:text-[#5865F2]"
- fill="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.419 0 1.334-.956 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.419 0 1.334-.946 2.419-2.157 2.419z"
- />
- </svg>
- </span>
- <span class="text-gray-700">Continue with Discord</span>
- </button>
+<div
+ class="w-full max-w-md transform-gpu space-y-8 rounded-2xl border border-gray-100 bg-white p-10 shadow-xl transition-all"
+>
+ <div class="text-center">
+ <div class="mx-auto flex h-24 w-24 items-center justify-center rounded-full">
+ <Logo class="h-full w-full" />
</div>
+ <h2 class="mt-6 text-3xl font-extrabold text-gray-900">sellershut</h2>
+ <p class="mt-2 text-sm text-gray-600">Please sign in to access the platform</p>
+ </div>
- <div class="mt-6 flex items-center justify-between text-xs">
- <span class="w-1/5 border-b border-gray-200"></span>
- <span class="text-gray-400 lowercase">sellershut.com</span>
- <span class="w-1/5 border-b border-gray-200"></span>
- </div>
+ <div class="mt-8 space-y-4">
+ <a
+ href={url('discord')}
+ target="_blank" rel="noopener noreferrer"
+ class="group relative flex w-full transform-gpu items-center justify-center rounded-xl border border-gray-900 bg-white px-4 py-3 text-sm font-medium text-gray-900 transition-all duration-200 hover:bg-gray-50 focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 focus:outline-none active:scale-[0.98]"
+ >
+ <span class="absolute inset-y-0 left-0 flex items-center pl-4">
+ <svg
+ class="h-5 w-5 text-gray-400 transition-colors duration-200 group-hover:text-[#5865F2]"
+ fill="currentColor"
+ viewBox="0 0 24 24"
+ >
+ <path
+ d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.419 0 1.334-.956 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.419 0 1.334-.946 2.419-2.157 2.419z"
+ />
+ </svg>
+ </span>
+ <span class="text-gray-700">Continue with Discord</span>
+ </a>
+ </div>
- <p class="text-center text-xs text-gray-500">
- By signing in, you agree to our
- <a
- href="/"
- class="font-medium text-rose-600 underline underline-offset-4 hover:text-rose-500"
- >Terms of Service</a
- >
- </p>
+ <div class="mt-6 flex items-center justify-between text-xs">
+ <span class="w-1/5 border-b border-gray-200"></span>
+ <span class="text-gray-400 lowercase">sellershut.com</span>
+ <span class="w-1/5 border-b border-gray-200"></span>
</div>
+
+ <p class="text-center text-xs text-gray-500">
+ By signing in, you agree to our
+ <a
+ href="/"
+ class="font-medium text-rose-600 underline underline-offset-4 hover:text-rose-500"
+ >Terms of Service</a
+ >
+ </p>
</div>
diff --git a/website/src/routes/welcome/+page.server.ts b/website/src/routes/welcome/+page.server.ts
new file mode 100644
index 0000000..503f361
--- /dev/null
+++ b/website/src/routes/welcome/+page.server.ts
@@ -0,0 +1,38 @@
+import { fail, redirect } from '@sveltejs/kit';
+import type { Actions, PageServerLoad } from './$types';
+import { profileSchema } from '$lib/schemas/profile';
+
+export const actions: Actions = {
+ default: async ({ request, fetch }) => {
+ console.log("hello");
+ const formData = await request.formData();
+ const data = Object.fromEntries(formData);
+
+ // 1. Zod Validation
+ const result = profileSchema.safeParse(data);
+
+ if (!result.success) {
+ return fail(400, {
+ errors: result.error.flatten().fieldErrors,
+ data: data as Record<string, string>
+ });
+ }
+
+ // 2. Example: Check availability against your backend
+ // Replace this with your actual backend URL
+ const response = await fetch(`/api/check-username?u=${result.data.username}`);
+ const { available } = await response.json();
+
+ if (!available) {
+ return fail(400, {
+ errors: { username: ["This username is already taken"] },
+ data: data as Record<string, string>
+ });
+ }
+
+ // 3. Success: Send to backend to create profile
+ // await fetch('...', { method: 'POST', body: JSON.stringify(result.data) });
+
+ throw redirect(303, '/dashboard');
+ }
+};
diff --git a/website/src/routes/welcome/+page.svelte b/website/src/routes/welcome/+page.svelte
new file mode 100644
index 0000000..863b69f
--- /dev/null
+++ b/website/src/routes/welcome/+page.svelte
@@ -0,0 +1,219 @@
+<script lang="ts">
+ import { enhance } from '$app/forms';
+ import type { ActionData } from './$types';
+
+ type FormFailure = {
+ errors: { username?: string[]; bio?: string[] };
+ data: { username?: string; bio?: string };
+ };
+ const domain = 'sellershut.com';
+
+ let { form }: { form: ActionData } = $props();
+
+ const formError = $derived(form && 'errors' in form ? (form as FormFailure) : null);
+ const errors = $derived(formError?.errors);
+
+ let username = $state('');
+ let bio = $state('');
+ let avatarPreview = $state<string | null>(null);
+
+ let canSubmit = $derived(username.trim().length >= 3);
+
+ function handleFileChange(e: Event) {
+ const target = e.target as HTMLInputElement;
+ if (target.files && target.files[0]) {
+ const reader = new FileReader();
+ reader.onload = (e) => (avatarPreview = e.target?.result as string);
+ reader.readAsDataURL(target.files[0]);
+ }
+ }
+ $effect(() => {
+ if (formError?.data?.username) username = formError.data.username;
+ if (formError?.data?.bio) bio = formError.data.bio;
+ });
+</script>
+
+<form
+ method="POST"
+ use:enhance
+ enctype="multipart/form-data"
+ class="w-full max-w-md overflow-hidden rounded-3xl border border-rose-100 bg-white shadow-xl"
+>
+ <div class="relative h-32 bg-linear-to-r from-rose-400 to-rose-600">
+ <div class="absolute -bottom-12 left-1/2 -translate-x-1/2">
+ <div class="group relative cursor-pointer">
+ <div
+ class="h-24 w-24 overflow-hidden rounded-full border-4 border-white bg-rose-100 shadow-md"
+ >
+ {#if avatarPreview}
+ <img src={avatarPreview} alt="Preview" class="h-full w-full object-cover" />
+ {:else}
+ <div class="flex h-full w-full items-center justify-center text-rose-300">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-10 w-10"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
+ />
+ </svg>
+ </div>
+ {/if}
+ </div>
+
+ <div
+ class="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 text-white"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+ >
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline
+ points="17 8 12 3 7 8"
+ /><line x1="12" y1="3" x2="12" y2="15" />
+ </svg>
+ </div>
+ <input
+ type="file"
+ name="avatar"
+ onchange={handleFileChange}
+ class="absolute inset-0 cursor-pointer opacity-0"
+ accept="image/*"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div class="px-8 pt-16 pb-10 text-center">
+ <h1 class="text-2xl font-bold text-gray-800">Final Touches</h1>
+ <p class="mt-1 text-sm text-gray-500">Tell the world a bit about yourself.</p>
+
+ <div class="mt-8 space-y-5 text-left">
+ <div>
+ <label for="email" class="mb-1 ml-1 block text-sm font-semibold text-gray-700">
+ Account Email
+ </label>
+
+ <div
+ class="flex cursor-not-allowed items-center rounded-xl border border-gray-200 bg-gray-50 transition-colors"
+ >
+ <span class="pl-4 text-gray-400">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-5 w-5"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
+ />
+ </svg>
+ </span>
+
+ <input
+ id="email"
+ name="email"
+ type="email"
+ value="email@domain.com"
+ readonly
+ tabindex="-1"
+ class="flex-1 cursor-not-allowed border-none bg-transparent py-2.5 pr-4 pl-3 text-sm text-gray-500 outline-none focus:ring-0 md:text-base"
+ />
+
+ <span class="pr-4 text-gray-300">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-4 w-4"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
+ />
+ </svg>
+ </span>
+ </div>
+ </div>
+ <div>
+ <label for="username" class="mb-1 ml-1 block text-sm font-semibold text-gray-700">
+ Username
+ </label>
+
+ <div
+ class="group flex items-center rounded-xl border border-gray-200 bg-white transition-all duration-200
+ focus-within:border-rose-400 focus-within:ring-4 focus-within:ring-rose-100
+ {errors?.username
+ ? 'border-red-500 focus-within:border-red-500 focus-within:ring-red-100'
+ : ''}"
+ >
+ <span class="pl-4 text-gray-400 select-none">@</span>
+
+ <input
+ id="username"
+ name="username"
+ type="text"
+ bind:value={username}
+ placeholder="username"
+ class="flex-1 border-none bg-transparent py-2.5 pr-2 pl-1 text-sm outline-none placeholder:text-gray-300 focus:ring-0 md:text-base"
+ />
+
+ <span
+ class="-ml-4 border-l border-gray-100 pr-4 pl-2 text-sm font-medium text-gray-400 select-none md:ml-2"
+ >
+ @{domain}
+ </span>
+ </div>
+
+ {#if errors?.username}
+ <p class="mt-1 ml-1 text-xs text-red-500">{errors.username[0]}</p>
+ {/if}
+ </div>
+ <div>
+ <label for="bio" class="mb-1 ml-1 block text-sm font-semibold text-gray-700"
+ >Bio</label
+ >
+ <textarea
+ id="bio"
+ name="bio"
+ bind:value={bio}
+ rows="3"
+ placeholder="I build cool things with Svelte..."
+ class="block w-full resize-none rounded-xl border border-gray-200 px-4
+ py-2.5 transition-all duration-200 outline-none
+ placeholder:text-gray-300 focus:border-rose-400 focus:ring-4 focus:ring-rose-100 focus:ring-offset-0
+ {errors?.bio ? 'border-red-500 focus:border-red-500 focus:ring-red-100' : ''}"
+ ></textarea>
+ {#if errors?.bio}
+ <p class="mt-1 ml-1 text-xs text-red-500">{errors.bio[0]}</p>
+ {/if}
+ </div>
+
+ <button
+ disabled={!canSubmit}
+ class="flex w-full justify-center rounded-md border border-transparent bg-rose-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-rose-700 focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 focus:outline-none disabled:bg-rose-300"
+ >
+ Finish Setup
+ </button>
+
+ <p class="mt-4 text-center text-[10px] tracking-widest text-gray-400">sellershut.com</p>
+ </div>
+ </div>
+</form>