diff options
| -rw-r--r-- | website/package.json | 3 | ||||
| -rw-r--r-- | website/pnpm-lock.yaml | 8 | ||||
| -rw-r--r-- | website/src/lib/schemas/profile.ts | 13 | ||||
| -rw-r--r-- | website/src/routes/+layout.svelte | 4 | ||||
| -rw-r--r-- | website/src/routes/+page.svelte | 4 | ||||
| -rw-r--r-- | website/src/routes/login/+page.svelte | 94 | ||||
| -rw-r--r-- | website/src/routes/welcome/+page.server.ts | 38 | ||||
| -rw-r--r-- | website/src/routes/welcome/+page.svelte | 219 |
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> |
