mirror of
https://github.com/BradNut/musicle-svelte
synced 2025-09-08 17:40:21 +00:00
257 lines
7.6 KiB
Svelte
257 lines
7.6 KiB
Svelte
<script module lang="ts">
|
|
import { z } from 'zod';
|
|
|
|
export const loginSchema = z.object({
|
|
email: z.string().email(),
|
|
});
|
|
|
|
export const loginPasswordSchema = z.object({
|
|
email: z.string().email(),
|
|
password: z.string({ required_error: 'Password is required' }),
|
|
})
|
|
|
|
export const verifySchema = z.object({
|
|
email: z.string().email(),
|
|
code: z.string().length(6)
|
|
});
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { Button } from '$lib/components/ui/button';
|
|
import * as Card from '$lib/components/ui/card';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { superForm, defaults, setError } from 'sveltekit-superforms';
|
|
import { zod } from 'sveltekit-superforms/adapters';
|
|
import * as Form from '$lib/components/ui/form';
|
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
import { goto } from '$app/navigation';
|
|
import ChevronLeftIcon from 'lucide-svelte/icons/chevron-left';
|
|
import { queryHandler } from '$lib/tanstack-query';
|
|
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
|
|
|
const RESEND_VERIFICATION_CODE_COOLDOWN = 60;
|
|
|
|
let queryClient = useQueryClient();
|
|
let step = $state<'login' | 'request' | 'verify'>('request');
|
|
let countdownTimer = $state(RESEND_VERIFICATION_CODE_COOLDOWN);
|
|
let resendVerificationCodeOnCooldown = $derived(
|
|
countdownTimer != RESEND_VERIFICATION_CODE_COOLDOWN
|
|
);
|
|
|
|
/* ----------------------------------- Api ---------------------------------- */
|
|
const requestUsernamePasswordLoginMutation = createMutation({
|
|
...queryHandler().iam.requestUsernamePasswordLogin(),
|
|
onSuccess(_data, variables, _context) {
|
|
step = 'verify';
|
|
$verifyForm.email = variables.json.email;
|
|
$verifyForm.
|
|
}
|
|
})
|
|
|
|
const requestMutation = createMutation({
|
|
...queryHandler().iam.requestLogin(),
|
|
onSuccess(_data, variables, _context) {
|
|
step = 'verify';
|
|
$verifyForm.email = variables.json.email;
|
|
startResendVerificationCodeTimer();
|
|
},
|
|
onError(error) {
|
|
requestErrors.set({ email: [error.message] });
|
|
}
|
|
});
|
|
|
|
const verifyMutation = createMutation({
|
|
...queryHandler().iam.verifyLogin(),
|
|
async onSuccess() {
|
|
await queryClient.invalidateQueries();
|
|
goto('/');
|
|
},
|
|
onError(error) {
|
|
verifyErrors.set({ code: [error.message] });
|
|
}
|
|
});
|
|
|
|
/* ------------------------------- Login Form ------------------------------- */
|
|
const sf_login_username_password = superForm(defaults(zod(loginPasswordSchema)), {
|
|
resetForm: false,
|
|
SPA: true,
|
|
validators: zod(loginPasswordSchema),
|
|
async onUpdated(event) {
|
|
if (!event.form.valid) return;
|
|
await $requestUsernamePasswordLogin.mutateAsync({ json: event.form.data });
|
|
}
|
|
})
|
|
|
|
const sf_login = superForm(defaults(zod(loginSchema)), {
|
|
resetForm: false,
|
|
SPA: true,
|
|
validators: zod(loginSchema),
|
|
async onUpdated(event) {
|
|
if (!event.form.valid) return;
|
|
|
|
await $requestMutation.mutateAsync({
|
|
json: {
|
|
email: event.form.data.email
|
|
}
|
|
});
|
|
|
|
if ($requestMutation.error)
|
|
return setError(event.form, 'email', $requestMutation.error.message);
|
|
|
|
step = 'verify';
|
|
$verifyForm.email = event.form.data.email;
|
|
}
|
|
});
|
|
const { form: loginPasswordForm, enhance: loginPasswordEnhance, errors: loginPasswordErrors } = sf_login_username_password;
|
|
|
|
const { form: loginForm, enhance: loginEnhance, errors: requestErrors } = sf_login;
|
|
|
|
/* ------------------------------- Verify Form ------------------------------ */
|
|
const sf_verify = superForm(defaults(zod(verifySchema)), {
|
|
resetForm: false,
|
|
SPA: true,
|
|
validators: zod(verifySchema),
|
|
async onUpdated(event) {
|
|
if (!event.form.valid) return;
|
|
await $verifyMutation.mutateAsync({ json: event.form.data });
|
|
}
|
|
});
|
|
const { form: verifyForm, enhance: verifyEnhance, errors: verifyErrors } = sf_verify;
|
|
|
|
/* -------------------------------- Functions ------------------------------- */
|
|
function startResendVerificationCodeTimer() {
|
|
countdownTimer = RESEND_VERIFICATION_CODE_COOLDOWN - 1;
|
|
const interval = setInterval(() => {
|
|
countdownTimer -= 1;
|
|
if (countdownTimer <= 0) {
|
|
countdownTimer = RESEND_VERIFICATION_CODE_COOLDOWN;
|
|
clearInterval(interval);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function resetAuthFlow() {
|
|
step = 'request';
|
|
sf_verify.reset();
|
|
sf_login.reset();
|
|
}
|
|
|
|
async function resendVerificationCode() {
|
|
startResendVerificationCodeTimer();
|
|
await $requestMutation.mutateAsync({
|
|
json: { email: $verifyForm.email }
|
|
});
|
|
}
|
|
</script>
|
|
|
|
{#if step === 'login'}
|
|
{@render loginCard()}
|
|
{/if}
|
|
{#if step === 'request'}
|
|
{@render requestsCard()}
|
|
{/if}
|
|
{#if step === 'verify'}
|
|
{@render verifyCard()}
|
|
{/if}
|
|
|
|
{#snippet loginCard()}
|
|
<Card.Root class="mx-auto w-full max-w-sm">
|
|
<Card.Header>
|
|
<Card.Title class="text-2xl">Login</Card.Title>
|
|
<Card.Description>Enter your email below to login to your account</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
<form use:loginEnhance method="POST" class="grid gap-4">
|
|
<Form.Field form={sf_login_username_password} name="email">
|
|
<Form.Control>
|
|
{#snippet children({ props })}
|
|
<Form.Label>Email</Form.Label>
|
|
<Input {...props} bind:value={$loginPasswordForm.email} />
|
|
{/snippet}
|
|
</Form.Control>
|
|
<Form.Description />
|
|
<Form.FieldErrors />
|
|
</Form.Field>
|
|
<Form.Field form={sf_login_username_password} name="password">
|
|
<Form.Control>
|
|
{#snippet children({ props })}
|
|
<Form.Label>Password</Form.Label>
|
|
<Input type="password" {...props} bind:value={$loginPasswordForm.password} />
|
|
{/snippet}
|
|
</Form.Control>
|
|
<Form.Description />
|
|
<Form.FieldErrors />
|
|
</Form.Field>
|
|
<Button type="submit" class="w-full">Continue with Email</Button>
|
|
</form>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
{/snippet}
|
|
|
|
{#snippet requestsCard()}
|
|
<Card.Root class="mx-auto w-full max-w-sm">
|
|
<Card.Header>
|
|
<Card.Title class="text-2xl">Login</Card.Title>
|
|
<Card.Description>Enter your email below to login to your account</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
<form use:loginEnhance method="POST" class="grid gap-4">
|
|
<Form.Field form={sf_login} name="email">
|
|
<Form.Control>
|
|
{#snippet children({ props })}
|
|
<Form.Label>Email</Form.Label>
|
|
<Input {...props} bind:value={$loginForm.email} />
|
|
{/snippet}
|
|
</Form.Control>
|
|
<Form.Description />
|
|
<Form.FieldErrors />
|
|
</Form.Field>
|
|
<Button type="submit" class="w-full">Continue with Email</Button>
|
|
</form>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
{/snippet}
|
|
|
|
{#snippet verifyCard()}
|
|
<Card.Root class="mx-auto w-full max-w-sm">
|
|
<Card.Header>
|
|
<Button onclick={resetAuthFlow} class="h-8 w-8" variant="secondary"
|
|
><ChevronLeftIcon /></Button
|
|
>
|
|
<Card.Title class="text-2xl">Enter Code</Card.Title>
|
|
<Card.Description
|
|
>Please enter the 6 digit code we sent to {$verifyForm.email}</Card.Description
|
|
>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
<form use:verifyEnhance method="POST" class="grid gap-4">
|
|
<input value={$verifyForm.email} id="email" hidden />
|
|
<Form.Field form={sf_verify} name="code">
|
|
<Form.Control>
|
|
{#snippet children({ props })}
|
|
<InputOTP.Root maxlength={6} {...props} bind:value={$verifyForm.code}>
|
|
{#snippet children({ cells })}
|
|
<InputOTP.Group>
|
|
{#each cells as cell}
|
|
<InputOTP.Slot {cell} />
|
|
{/each}
|
|
</InputOTP.Group>
|
|
{/snippet}
|
|
</InputOTP.Root>
|
|
{/snippet}
|
|
</Form.Control>
|
|
<Form.Description />
|
|
<Form.FieldErrors />
|
|
</Form.Field>
|
|
<Button type="submit" class="w-full">Verify</Button>
|
|
</form>
|
|
<div class="mt-4 text-sm">
|
|
{#if resendVerificationCodeOnCooldown}
|
|
<p>Resend code in {countdownTimer}s</p>
|
|
{:else}
|
|
<Button onclick={resendVerificationCode} variant="ghost" size="sm">Resend Code</Button>
|
|
{/if}
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
{/snippet}
|