mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Merge pull request #13 from BradNut/shadcn-auth-refactor
Shadcn auth refactor
This commit is contained in:
commit
998855c212
36 changed files with 555 additions and 323 deletions
|
|
@ -2,7 +2,7 @@
|
|||
import { enhance } from '$app/forms';
|
||||
import { LogOut } from 'lucide-svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Toggle } from "$lib/components/ui/toggle";
|
||||
// import { Toggle } from "$lib/components/ui/toggle"; -- TODO: Add light/dark toggle
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
import * as Sheet from "$lib/components/ui/sheet";
|
||||
import Logo from '$components/logo.svelte';
|
||||
|
|
@ -34,9 +34,6 @@
|
|||
<Sheet.Content side="right">
|
||||
<Sheet.Header>
|
||||
<Sheet.Title>Menu</Sheet.Title>
|
||||
<Toggle aria-label="toggle bold">
|
||||
|
||||
</Toggle>
|
||||
</Sheet.Header>
|
||||
<div class="menu">
|
||||
<Sheet.Close asChild let:builder>
|
||||
|
|
@ -61,10 +58,12 @@
|
|||
action="/logout"
|
||||
method="POST"
|
||||
>
|
||||
<Button type="submit">
|
||||
<LogOut class="mr-2 h-4 w-4"/>
|
||||
Sign out
|
||||
</Button>
|
||||
<Sheet.Close asChild let:builder>
|
||||
<Button builders={[builder]} type="submit">
|
||||
<LogOut class="mr-2 h-4 w-4"/>
|
||||
Sign out
|
||||
</Button>
|
||||
</Sheet.Close>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
|
||||
export let value: HTMLInputAttributes["value"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:paste
|
||||
on:input
|
||||
{...$$restProps}
|
||||
/>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { cva } from "class-variance-authority";
|
||||
import { Label as LabelPrimitive } from "radix-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
);
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root class={cn(labelVariants(), className)} {...$$restProps}>
|
||||
<slot />
|
||||
</LabelPrimitive.Root>
|
||||
|
|
@ -1 +1,7 @@
|
|||
export { default as Label } from "./Label.svelte";
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label
|
||||
};
|
||||
|
|
|
|||
21
src/lib/components/ui/label/label.svelte
Normal file
21
src/lib/components/ui/label/label.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = LabelPrimitive.Props;
|
||||
type $$Events = LabelPrimitive.Events;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:mousedown
|
||||
>
|
||||
<slot />
|
||||
</LabelPrimitive.Root>
|
||||
0
src/routes/(app)/(protected)/admin/+page.svelte
Normal file
0
src/routes/(app)/(protected)/admin/+page.svelte
Normal file
|
|
@ -3,8 +3,8 @@
|
|||
//import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
||||
import { userSchema } from '$lib/config/zod-schemas';
|
||||
import { AlertTriangle } from 'lucide-svelte';
|
||||
import Label from '$components/ui/label/Label.svelte';
|
||||
import Input from '$components/ui/input/Input.svelte';
|
||||
import { Label } from '$components/ui/label';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { Button } from '$components/ui/button';
|
||||
export let data;
|
||||
|
||||
198
src/routes/(app)/+layout.svelte
Normal file
198
src/routes/(app)/+layout.svelte
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<script lang="ts">
|
||||
import 'iconify-icon';
|
||||
import Transition from '$lib/components/transition/index.svelte';
|
||||
import Header from '$lib/components/header/index.svelte';
|
||||
import Footer from '$lib/components/footer.svelte';
|
||||
|
||||
// $: {
|
||||
// if ($navigating) {
|
||||
// debounce(() => {
|
||||
// boredState.update((n) => ({ ...n, loading: true }));
|
||||
// }, 250);
|
||||
// }
|
||||
// if (!$navigating) {
|
||||
// boredState.update((n) => ({ ...n, loading: false }));
|
||||
// }
|
||||
// }
|
||||
|
||||
// $: isOpen = $boredState?.dialog?.isOpen;
|
||||
|
||||
// if (browser) {
|
||||
// const collator = new Intl.Collator('en');
|
||||
|
||||
// let collectionEmpty = $collectionStore.length === 0 || false;
|
||||
// let wishlistEmpty = $wishlistStore.length === 0 || false;
|
||||
// if (wishlistEmpty && localStorage?.wishlist && localStorage?.wishlist?.length !== 0) {
|
||||
// const wishlist: SavedGameType[] = JSON.parse(localStorage.wishlist);
|
||||
// if (wishlist?.length !== 0) {
|
||||
// wishlist.sort((a, b) => collator.compare(a.name, b.name));
|
||||
// for (const item of wishlist) {
|
||||
// if (!item?.searchTerms) {
|
||||
// item.searchTerms = `${item?.name?.toLowerCase()}`;
|
||||
// }
|
||||
// if (!item?.includeInRandom) {
|
||||
// item.includeInRandom = false;
|
||||
// }
|
||||
// }
|
||||
// wishlistStore.addAll(wishlist);
|
||||
// }
|
||||
// }
|
||||
// if (collectionEmpty && localStorage?.collection && localStorage?.collection?.length !== 0) {
|
||||
// const collection: SavedGameType[] = JSON.parse(localStorage.collection);
|
||||
// if (collection?.length !== 0) {
|
||||
// collection.sort((a, b) => collator.compare(a.name, b.name));
|
||||
// for (const item of collection) {
|
||||
// if (!item?.searchTerms) {
|
||||
// item.searchTerms = `${item?.name?.toLowerCase()}`;
|
||||
// }
|
||||
// }
|
||||
// collectionStore.addAll(collection);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export let data;
|
||||
// $: ({ user } = data);
|
||||
|
||||
// const flash = getFlash(page, {
|
||||
// clearAfterMs: 6000
|
||||
// });
|
||||
// let flashType;
|
||||
// let flashMessage;
|
||||
// $: flashType = $flash?.type;
|
||||
// $: flashMessage = $flash?.message;
|
||||
|
||||
// if ($flash && flashType && flashMessage) {
|
||||
// switch (flashType) {
|
||||
// case 'success':
|
||||
// toast.success(flashMessage);
|
||||
// break;
|
||||
// case 'error':
|
||||
// toast.error(flashMessage);
|
||||
// break;
|
||||
// default:
|
||||
// toast.error(flashMessage);
|
||||
// }
|
||||
// }
|
||||
|
||||
// onMount(() => {
|
||||
// // set the theme to the user's active theme
|
||||
// $theme = user?.theme || 'system';
|
||||
// document.querySelector('html')?.setAttribute('data-theme', $theme);
|
||||
// });
|
||||
|
||||
// flash.subscribe(($flash) => {
|
||||
// if (!$flash) return;
|
||||
|
||||
// if ($flash.type == 'success') {
|
||||
// toast.success($flash.message);
|
||||
// } else {
|
||||
// toast.error($flash.message, {
|
||||
// duration: 5000
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Clearing the flash message could sometimes
|
||||
// // be required here to avoid double-toasting.
|
||||
// flash.set(undefined);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<!-- {#if !dev}
|
||||
<Analytics />
|
||||
{/if} -->
|
||||
|
||||
<!-- <div class="wrapper"> -->
|
||||
<Header user={data.user} />
|
||||
|
||||
<main>
|
||||
<Transition url={data.url} transition={{ type: 'page' }}>
|
||||
<slot />
|
||||
</Transition>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- <Toaster /> -->
|
||||
|
||||
<!-- {#if $boredState?.loading}
|
||||
<Portal>
|
||||
<div class="loading">
|
||||
<Loading></Loading>
|
||||
<h3>Loading...</h3>
|
||||
</div>
|
||||
<div class="background"></div>
|
||||
</Portal>
|
||||
{/if}
|
||||
{#if isOpen}
|
||||
<div class="container">
|
||||
<svelte:component this={$boredState?.dialog?.content}></svelte:component>
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
<style lang="postcss">
|
||||
/* .flash {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
place-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 2px;
|
||||
} */
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 101;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
& h3 {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
background: black;
|
||||
opacity: 0.8;
|
||||
cursor: none;
|
||||
inset: 0;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* .wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-height: 100vh;
|
||||
} */
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0rem;
|
||||
max-width: 80vw;
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
max-width: 70vw;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(.dialog-overlay) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import prisma from '$lib/prisma.js';
|
||||
import { boardGameApi } from '../../api';
|
||||
|
||||
export const load = async ({ params, setHeaders, locals }) => {
|
||||
try {
|
||||
158
src/routes/(auth)/+layout.svelte
Normal file
158
src/routes/(auth)/+layout.svelte
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Logo from "$lib/components/logo.svelte";
|
||||
|
||||
$: link = {
|
||||
label: $page.url.pathname === "/sign-up" ? "Login" : "Sign up",
|
||||
href: $page.url.pathname === "/sign-up" ? "/login" : "/sign-up"
|
||||
};
|
||||
|
||||
$: console.log($page.url.pathname);
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<Button
|
||||
href={$page.url.pathname === "/sign-up" ? "/login" : "/sign-up"}
|
||||
variant="ghost"
|
||||
class="auth-button"
|
||||
>
|
||||
{#if $page.url.pathname === "/sign-up"}
|
||||
Login
|
||||
{:else}
|
||||
Sign up
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="auth-marketing">
|
||||
<div
|
||||
class="image"
|
||||
style="
|
||||
background-image:
|
||||
url(https://images.unsplash.com/photo-1588591795084-1770cb3be374?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
|
||||
/>
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-image">
|
||||
<Logo />
|
||||
</div>
|
||||
Bored Game
|
||||
</a>
|
||||
<div class="quote-wrapper">
|
||||
<blockquote class="quote">
|
||||
<p>
|
||||
"How many games do I own? What was the last one I played? What haven't I played in a long time? If this sounds like you then Bored Game is your new best friend."
|
||||
</p>
|
||||
<footer>Bradley</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-form">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
/* display: none;
|
||||
position: relative; */
|
||||
display: flex;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: grid
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.auth-marketing {
|
||||
display: none;
|
||||
position: relative;
|
||||
padding: 2.5rem;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: #ffffff;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 500;
|
||||
transition-property: color, background-color, border-color,text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-wrapper {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
margin-top: auto;
|
||||
|
||||
.quote {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
p {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
@media (min-width: 1024px) {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.auth-button) {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -46,18 +46,16 @@
|
|||
|
||||
<div class="login">
|
||||
<form method="POST" use:enhance>
|
||||
<div class="grid w-full max-w-sm items-center gap-2">
|
||||
<h2
|
||||
class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
|
||||
>
|
||||
Log into your account
|
||||
</h2>
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} required />
|
||||
<Label for="password">Password</Label>
|
||||
<Input type="password" id="password" name="password" placeholder="Password" autocomplete="password" data-invalid={$errors.password} bind:value={$form.password} required />
|
||||
<Button type="submit">Sign In</Button>
|
||||
</div>
|
||||
<h2
|
||||
class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
|
||||
>
|
||||
Log into your account
|
||||
</h2>
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} required />
|
||||
<Label for="password">Password</Label>
|
||||
<Input type="password" id="password" name="password" placeholder="Password" autocomplete="password" data-invalid={$errors.password} bind:value={$form.password} required />
|
||||
<Button type="submit">Login</Button>
|
||||
</form>
|
||||
{#if $errors._errors}
|
||||
<Alert.Root variant="destructive">
|
||||
|
|
@ -68,11 +66,36 @@
|
|||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<p class="px-8 text-center text-sm text-muted-foreground">
|
||||
By clicking continue, you agree to our
|
||||
<a href="/terms-of-service" class="underline underline-offset-4 hover:text-primary">
|
||||
Terms of Service
|
||||
</a>
|
||||
and
|
||||
<a href="/privacy" class="underline underline-offset-4 hover:text-primary"> Privacy Policy </a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.login {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
display: flex;
|
||||
margin-top: 1.5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
import toast from 'svelte-french-toast';
|
||||
import { ChevronsUpDown } from "lucide-svelte";
|
||||
import { Button } from '$components/ui/button';
|
||||
import { Input } from '$components/ui/input';
|
||||
import Label from '$components/ui/label/Label.svelte';
|
||||
import { Label } from '$components/ui/label';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { signUpSchema } from '$lib/config/zod-schemas.js';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
|
|
@ -43,82 +43,113 @@
|
|||
<title>Bored Game | Sign Up</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<div class="sign-up">
|
||||
<form method="POST" action="/sign-up" use:enhance>
|
||||
<div class="grid w-full max-w-sm items-center gap-2.5">
|
||||
<h2
|
||||
class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
|
||||
>
|
||||
Signup for an account
|
||||
</h2>
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />
|
||||
{#if $errors.username}
|
||||
<p class="text-sm text-destructive">{$errors.username}</p>
|
||||
{/if}
|
||||
<Label for="password">Password</Label>
|
||||
<Input type="password" id="password" class={$errors.password && "outline outline-destructive"} name="password" placeholder="Password" autocomplete="new-password" data-invalid={$errors.password} bind:value={$form.password} />
|
||||
{#if $errors.password}
|
||||
<p class="text-sm text-destructive">{$errors.password}</p>
|
||||
{/if}
|
||||
<Label for="confirm_password">Confirm Password</Label>
|
||||
<Input type="password" id="confirm_password" class={$errors.confirm_password && "outline outline-destructive"} name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" data-invalid={$errors.confirm_password} bind:value={$form.confirm_password} />
|
||||
{#if $errors.confirm_password}
|
||||
<p class="text-sm text-destructive">{$errors.confirm_password}</p>
|
||||
{/if}
|
||||
<Collapsible.Root class="grid w-full max-w-sm items-center gap-2.5">
|
||||
<div>
|
||||
Optional Fields:
|
||||
<Collapsible.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" size="sm" type="button" class="w-9 p-0">
|
||||
<ChevronsUpDown class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="email">Email</Label>
|
||||
<Input type="email" id="email" class={$errors.email && "outline outline-destructive"} name="email" placeholder="Email" autocomplete="email" data-invalid={$errors.email} bind:value={$form.email} />
|
||||
{#if $errors.email}
|
||||
<p class="text-sm text-destructive">{$errors.email}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">First Name</Label>
|
||||
<Input type="text" id="firstName" class={$errors.firstName && "outline outline-destructive"} name="firstName" placeholder="First Name" autocomplete="given-name" data-invalid={$errors.firstName} bind:value={$form.firstName} />
|
||||
{#if $errors.firstName}
|
||||
<p class="text-sm text-destructive">{$errors.firstName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">Last Name</Label>
|
||||
<Input type="text" id="lastName" class={$errors.firstName && "outline outline-destructive"} name="lastName" placeholder="Last Name" autocomplete="family-name" data-invalid={$errors.lastName} bind:value={$form.lastName} />
|
||||
{#if $errors.lastName}
|
||||
<p class="text-sm text-destructive">{$errors.lastName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
<div class="grid grid-cols-2">
|
||||
<Button type="submit">Signup</Button>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/">or Cancel</Button>
|
||||
<h2>Signup for an account</h2>
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username" placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />
|
||||
{#if $errors.username}
|
||||
<p class="text-sm text-destructive">{$errors.username}</p>
|
||||
{/if}
|
||||
<Label for="password">Password</Label>
|
||||
<Input type="password" id="password" class={$errors.password && "outline outline-destructive"} name="password" placeholder="Password" autocomplete="new-password" data-invalid={$errors.password} bind:value={$form.password} />
|
||||
{#if $errors.password}
|
||||
<p class="text-sm text-destructive">{$errors.password}</p>
|
||||
{/if}
|
||||
<Label for="confirm_password">Confirm Password</Label>
|
||||
<Input type="password" id="confirm_password" class={$errors.confirm_password && "outline outline-destructive"} name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" data-invalid={$errors.confirm_password} bind:value={$form.confirm_password} />
|
||||
{#if $errors.confirm_password}
|
||||
<p class="text-sm text-destructive">{$errors.confirm_password}</p>
|
||||
{/if}
|
||||
<Collapsible.Root class="grid w-full max-w-sm items-center gap-2.5">
|
||||
<div>
|
||||
Optional Fields:
|
||||
<Collapsible.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" size="sm" type="button" class="w-9 p-0">
|
||||
<ChevronsUpDown class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
{#if !$form.email}
|
||||
<Alert.Root>
|
||||
<Alert.Title level="h3">Heads up!</Alert.Title>
|
||||
<Alert.Description>
|
||||
Without an email address, you won't be able to reset your password. Submit only if you are sure. You can always add this later.
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="email">Email</Label>
|
||||
<Input type="email" id="email" class={$errors.email && "outline outline-destructive"} name="email" placeholder="Email" autocomplete="email" data-invalid={$errors.email} bind:value={$form.email} />
|
||||
{#if $errors.email}
|
||||
<p class="text-sm text-destructive">{$errors.email}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">First Name</Label>
|
||||
<Input type="text" id="firstName" class={$errors.firstName && "outline outline-destructive"} name="firstName" placeholder="First Name" autocomplete="given-name" data-invalid={$errors.firstName} bind:value={$form.firstName} />
|
||||
{#if $errors.firstName}
|
||||
<p class="text-sm text-destructive">{$errors.firstName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">Last Name</Label>
|
||||
<Input type="text" id="lastName" class={$errors.firstName && "outline outline-destructive"} name="lastName" placeholder="Last Name" autocomplete="family-name" data-invalid={$errors.lastName} bind:value={$form.lastName} />
|
||||
{#if $errors.lastName}
|
||||
<p class="text-sm text-destructive">{$errors.lastName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
<div class="grid grid-cols-2">
|
||||
<Button type="submit">Signup</Button>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/">or Cancel</Button>
|
||||
</div>
|
||||
{#if !$form.email}
|
||||
<Alert.Root>
|
||||
<Alert.Title level="h3">Heads up!</Alert.Title>
|
||||
<Alert.Description>
|
||||
Without an email address, you won't be able to reset your password. Submit only if you are sure. You can always add this later.
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
<style lang="postcss">
|
||||
.sign-up {
|
||||
display: flex;
|
||||
margin-top: 1.5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
|
||||
h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom-width: 1px;
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
scroll-margin: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,70 +1,14 @@
|
|||
<script lang="ts">
|
||||
import "../app.postcss";
|
||||
import "$lib/styles/app.postcss";
|
||||
import { onMount } from "svelte";
|
||||
import { getFlash } from 'sveltekit-flash-message/client';
|
||||
import toast, { Toaster } from 'svelte-french-toast';
|
||||
import { navigating, page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import debounce from 'just-debounce-it';
|
||||
import 'iconify-icon';
|
||||
import Transition from '$lib/components/transition/index.svelte';
|
||||
import Analytics from '$lib/components/analytics.svelte';
|
||||
import Header from '$lib/components/header/index.svelte';
|
||||
import Footer from '$lib/components/footer.svelte';
|
||||
import Loading from '$lib/components/loading.svelte';
|
||||
import Portal from '$lib/Portal.svelte';
|
||||
import { boredState } from '$lib/stores/boredState';
|
||||
import { collectionStore } from '$lib/stores/collectionStore';
|
||||
import { wishlistStore } from '$lib/stores/wishlistStore';
|
||||
import { theme } from '$state/theme';
|
||||
import type { SavedGameType } from '$lib/types';
|
||||
|
||||
$: {
|
||||
if ($navigating) {
|
||||
debounce(() => {
|
||||
boredState.update((n) => ({ ...n, loading: true }));
|
||||
}, 250);
|
||||
}
|
||||
if (!$navigating) {
|
||||
boredState.update((n) => ({ ...n, loading: false }));
|
||||
}
|
||||
}
|
||||
|
||||
$: isOpen = $boredState?.dialog?.isOpen;
|
||||
|
||||
if (browser) {
|
||||
const collator = new Intl.Collator('en');
|
||||
|
||||
let collectionEmpty = $collectionStore.length === 0 || false;
|
||||
let wishlistEmpty = $wishlistStore.length === 0 || false;
|
||||
if (wishlistEmpty && localStorage?.wishlist && localStorage?.wishlist?.length !== 0) {
|
||||
const wishlist: SavedGameType[] = JSON.parse(localStorage.wishlist);
|
||||
if (wishlist?.length !== 0) {
|
||||
wishlist.sort((a, b) => collator.compare(a.name, b.name));
|
||||
for (const item of wishlist) {
|
||||
if (!item?.searchTerms) {
|
||||
item.searchTerms = `${item?.name?.toLowerCase()}`;
|
||||
}
|
||||
if (!item?.includeInRandom) {
|
||||
item.includeInRandom = false;
|
||||
}
|
||||
}
|
||||
wishlistStore.addAll(wishlist);
|
||||
}
|
||||
}
|
||||
if (collectionEmpty && localStorage?.collection && localStorage?.collection?.length !== 0) {
|
||||
const collection: SavedGameType[] = JSON.parse(localStorage.collection);
|
||||
if (collection?.length !== 0) {
|
||||
collection.sort((a, b) => collator.compare(a.name, b.name));
|
||||
for (const item of collection) {
|
||||
if (!item?.searchTerms) {
|
||||
item.searchTerms = `${item?.name?.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
collectionStore.addAll(collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
|
|
@ -79,18 +23,18 @@
|
|||
$: flashType = $flash?.type;
|
||||
$: flashMessage = $flash?.message;
|
||||
|
||||
// if ($flash && flashType && flashMessage) {
|
||||
// switch (flashType) {
|
||||
// case 'success':
|
||||
// toast.success(flashMessage);
|
||||
// break;
|
||||
// case 'error':
|
||||
// toast.error(flashMessage);
|
||||
// break;
|
||||
// default:
|
||||
// toast.error(flashMessage);
|
||||
// }
|
||||
// }
|
||||
$: {
|
||||
if ($navigating) {
|
||||
debounce(() => {
|
||||
boredState.update((n) => ({ ...n, loading: true }));
|
||||
}, 250);
|
||||
}
|
||||
if (!$navigating) {
|
||||
boredState.update((n) => ({ ...n, loading: false }));
|
||||
}
|
||||
}
|
||||
|
||||
$: isOpen = $boredState?.dialog?.isOpen;
|
||||
|
||||
onMount(() => {
|
||||
// set the theme to the user's active theme
|
||||
|
|
@ -119,95 +63,17 @@
|
|||
<Analytics />
|
||||
{/if}
|
||||
|
||||
<div class="wrapper">
|
||||
<Header user={data.user} />
|
||||
|
||||
<main>
|
||||
<Transition url={data.url} transition={{ type: 'page' }}>
|
||||
<slot />
|
||||
</Transition>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<div class="layout">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
|
||||
{#if $boredState?.loading}
|
||||
<Portal>
|
||||
<div class="loading">
|
||||
<Loading></Loading>
|
||||
<h3>Loading...</h3>
|
||||
</div>
|
||||
<div class="background"></div>
|
||||
</Portal>
|
||||
{/if}
|
||||
{#if isOpen}
|
||||
<div class="container">
|
||||
<svelte:component this={$boredState?.dialog?.content}></svelte:component>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.flash {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
place-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 101;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
& h3 {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
background: black;
|
||||
opacity: 0.8;
|
||||
cursor: none;
|
||||
inset: 0;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
.layout {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0rem;
|
||||
max-width: 80vw;
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
max-width: 70vw;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(.dialog-overlay) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
const base = 'https://api.boardgameatlas.com/api';
|
||||
|
||||
export function boardGameApi(
|
||||
method: string,
|
||||
resource: string,
|
||||
queryParams: Record<string, string>,
|
||||
data?: Record<string, unknown>
|
||||
) {
|
||||
// console.log('queryParams', queryParams);
|
||||
queryParams.client_id = BOARD_GAME_ATLAS_CLIENT_ID;
|
||||
const urlQueryParams = new URLSearchParams(queryParams);
|
||||
const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`;
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: data && JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue