mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add svelte
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { createForm } from '@tanstack/svelte-form';
|
||||
import { z } from 'zod';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { switchToSignUp } = $props<{ switchToSignUp: () => void }>();
|
||||
|
||||
const validationSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
});
|
||||
|
||||
const form = createForm(() => ({
|
||||
defaultValues: { email: '', password: '' },
|
||||
onSubmit: async ({ value }) => {
|
||||
await authClient.signIn.email(
|
||||
{ email: value.email, password: value.password },
|
||||
{
|
||||
onSuccess: () => goto('/dashboard'),
|
||||
onError: (error) => {
|
||||
console.log(error.error.message || 'Sign in failed. Please try again.');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
validators: {
|
||||
onSubmit: validationSchema,
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-10 w-full max-w-md p-6">
|
||||
<h1 class="mb-6 text-center font-bold text-3xl">Welcome Back</h1>
|
||||
|
||||
<form
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form.Field name="email">
|
||||
{#snippet children(field)}
|
||||
<div class="space-y-1">
|
||||
<label for={field.name}>Email</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
class="w-full border"
|
||||
onblur={field.handleBlur}
|
||||
value={field.state.value}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
field.handleChange(target.value)
|
||||
}} />
|
||||
{#if field.state.meta.isTouched}
|
||||
{#each field.state.meta.errors as error}
|
||||
<p class="text-sm text-red-500" role="alert">{error}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="password">
|
||||
{#snippet children(field)}
|
||||
<div class="space-y-1">
|
||||
<label for={field.name}>Password</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
class="w-full border"
|
||||
onblur={field.handleBlur}
|
||||
value={field.state.value}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
field.handleChange(target.value)
|
||||
}}
|
||||
/>
|
||||
{#if field.state.meta.isTouched}
|
||||
{#each field.state.meta.errors as error}
|
||||
<p class="text-sm text-red-500" role="alert">{error}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
|
||||
{#snippet children(state)}
|
||||
<button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
|
||||
{state.isSubmitting ? 'Submitting...' : 'Sign In'}
|
||||
</button>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignUp}>
|
||||
Need an account? Sign Up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { createForm } from '@tanstack/svelte-form';
|
||||
import { z } from 'zod';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { switchToSignIn } = $props<{ switchToSignIn: () => void }>();
|
||||
|
||||
const validationSchema = z.object({
|
||||
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
});
|
||||
|
||||
|
||||
const form = createForm(() => ({
|
||||
defaultValues: { name: '', email: '', password: '' },
|
||||
onSubmit: async ({ value }) => {
|
||||
await authClient.signUp.email(
|
||||
{
|
||||
email: value.email,
|
||||
password: value.password,
|
||||
name: value.name,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
goto('/dashboard');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error.error.message || 'Sign up failed. Please try again.');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
validators: {
|
||||
onSubmit: validationSchema,
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-10 w-full max-w-md p-6">
|
||||
<h1 class="mb-6 text-center font-bold text-3xl">Create Account</h1>
|
||||
|
||||
<form
|
||||
id="form"
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form.Field name="name">
|
||||
{#snippet children(field)}
|
||||
<div class="space-y-1">
|
||||
<label for={field.name}>Name</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
class="w-full border"
|
||||
onblur={field.handleBlur}
|
||||
value={field.state.value}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
field.handleChange(target.value)
|
||||
}}
|
||||
/>
|
||||
{#if field.state.meta.isTouched}
|
||||
{#each field.state.meta.errors as error}
|
||||
<p class="text-sm text-red-500" role="alert">{error}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="email">
|
||||
{#snippet children(field)}
|
||||
<div class="space-y-1">
|
||||
<label for={field.name}>Email</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
class="w-full border"
|
||||
onblur={field.handleBlur}
|
||||
value={field.state.value}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
field.handleChange(target.value)
|
||||
}}
|
||||
/>
|
||||
{#if field.state.meta.isTouched}
|
||||
{#each field.state.meta.errors as error}
|
||||
<p class="text-sm text-red-500" role="alert">{error}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="password">
|
||||
{#snippet children(field)}
|
||||
<div class="space-y-1">
|
||||
<label for={field.name}>Password</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
class="w-full border"
|
||||
onblur={field.handleBlur}
|
||||
value={field.state.value}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
field.handleChange(target.value)
|
||||
}}
|
||||
/>
|
||||
{#if field.state.meta.errors}
|
||||
{#each field.state.meta.errors as error}
|
||||
<p class="text-sm text-red-500" role="alert">{error}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
|
||||
{#snippet children(state)}
|
||||
<button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
|
||||
{state.isSubmitting ? 'Submitting...' : 'Sign Up'}
|
||||
</button>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignIn}>
|
||||
Already have an account? Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { goto } from '$app/navigation';
|
||||
import { queryClient } from '$lib/orpc';
|
||||
|
||||
const sessionQuery = authClient.useSession();
|
||||
|
||||
async function handleSignOut() {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries();
|
||||
goto('/');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Sign out failed:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
{#if $sessionQuery.isPending}
|
||||
<div class="h-8 w-24 animate-pulse rounded bg-neutral-700"></div>
|
||||
{:else if $sessionQuery.data?.user}
|
||||
{@const user = $sessionQuery.data.user}
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-neutral-300 hidden sm:inline" title={user.email}>
|
||||
{user.name || user.email?.split('@')[0] || 'User'}
|
||||
</span>
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="rounded px-3 py-1 text-sm bg-red-600 hover:bg-red-700 text-white transition-colors"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={goToLogin}
|
||||
class="rounded px-3 py-1 text-sm bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PUBLIC_SERVER_URL } from "$env/static/public";
|
||||
import { createAuthClient } from "better-auth/svelte";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: PUBLIC_SERVER_URL,
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { orpc } from '$lib/orpc';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const sessionQuery = authClient.useSession();
|
||||
|
||||
const privateDataQuery = createQuery(orpc.privateData.queryOptions());
|
||||
|
||||
onMount(() => {
|
||||
const { data: session, isPending } = get(sessionQuery);
|
||||
if (!session && !isPending) {
|
||||
goto('/login');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $sessionQuery.isPending}
|
||||
<div>Loading...</div>
|
||||
{:else if !$sessionQuery.data}
|
||||
<!-- Redirecting... -->
|
||||
{:else}
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome {$sessionQuery.data.user.name}</p>
|
||||
<p>privateData: {$privateDataQuery.data?.message}</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import SignInForm from '../../components/SignInForm.svelte';
|
||||
import SignUpForm from '../../components/SignUpForm.svelte';
|
||||
|
||||
let showSignIn = $state(true);
|
||||
</script>
|
||||
|
||||
{#if showSignIn}
|
||||
<SignInForm switchToSignUp={() => showSignIn = false} />
|
||||
{:else}
|
||||
<SignUpForm switchToSignIn={() => showSignIn = true} />
|
||||
{/if}
|
||||
Reference in New Issue
Block a user