Skip to content

Commit ffe01de

Browse files
committed
feat: login to create template
1 parent 7b7b390 commit ffe01de

File tree

10 files changed

+523
-456
lines changed

10 files changed

+523
-456
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script lang="ts">
2+
import * as Tabs from "$lib/components/ui/tabs"
3+
import Login from "./login.svelte"
4+
import Signup from "./signup.svelte"
5+
6+
export let registrationEnabled: boolean
7+
export let redirect: string | null
8+
export let onSuccess: () => void = () => {}
9+
export let githubEnabled: boolean
10+
export let googleEnabled: boolean
11+
export let invitationId: string | null
12+
export let email: string | null
13+
</script>
14+
15+
<Tabs.Root value="login" class="w-full">
16+
<Tabs.List class="w-full">
17+
<Tabs.Trigger value="login" class="flex-1">Login</Tabs.Trigger>
18+
<Tabs.Trigger value="signup" class="flex-1">Signup</Tabs.Trigger>
19+
</Tabs.List>
20+
<Tabs.Content value="login">
21+
<Login {registrationEnabled} {redirect} {onSuccess} {githubEnabled} {googleEnabled} />
22+
</Tabs.Content>
23+
<Tabs.Content value="signup">
24+
<Signup {redirect} {onSuccess} {githubEnabled} {googleEnabled} {invitationId} {email} />
25+
</Tabs.Content>
26+
</Tabs.Root>
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<script lang="ts">
2+
import { goto } from "$app/navigation"
3+
import { Input } from "$lib/components/ui/input/index.js"
4+
import { Label } from "$lib/components/ui/label/index.js"
5+
import Github from "$lib/images/github.svg"
6+
import Google from "$lib/images/Google.svg"
7+
import { createMutation } from "@tanstack/svelte-query"
8+
import { z } from "@undb/zod"
9+
import { defaults, superForm } from "sveltekit-superforms"
10+
import { zodClient } from "sveltekit-superforms/adapters"
11+
import * as Form from "$lib/components/ui/form"
12+
import { Button } from "$lib/components/ui/button"
13+
import { Separator } from "$lib/components/ui/separator"
14+
import PasswordInput from "$lib/components/ui/input/password-input.svelte"
15+
import * as Alert from "$lib/components/ui/alert/index.js"
16+
import autoAnimate from "@formkit/auto-animate"
17+
import { LoaderCircleIcon } from "lucide-svelte"
18+
import ResetPassword from "$lib/components/blocks/auth/reset-password.svelte"
19+
import { page } from "$app/stores"
20+
21+
export let registrationEnabled: boolean
22+
export let redirect: string | null
23+
export let onSuccess: () => void = () => {}
24+
export let githubEnabled: boolean
25+
export let googleEnabled: boolean
26+
27+
const schema = z.object({
28+
email: z.string().email(),
29+
password: z.string(),
30+
})
31+
32+
type LoginSchema = z.infer<typeof schema>
33+
34+
let loginError = false
35+
36+
const loginMutation = createMutation({
37+
mutationFn: async (input: LoginSchema) => {
38+
try {
39+
const { ok } = await fetch("/api/login", { method: "POST", body: JSON.stringify(input) })
40+
if (!ok) {
41+
throw new Error("Failed to login")
42+
}
43+
return
44+
} catch (error) {
45+
loginError = true
46+
}
47+
},
48+
onMutate(variables) {
49+
loginError = false
50+
},
51+
async onSuccess(data, variables, context) {
52+
onSuccess()
53+
},
54+
async onError(error, variables, context) {
55+
loginError = true
56+
},
57+
})
58+
59+
const form = superForm(
60+
defaults(
61+
{
62+
email: $page.url.searchParams.get("email") ?? "",
63+
password: "",
64+
},
65+
zodClient(schema),
66+
),
67+
{
68+
SPA: true,
69+
dataType: "json",
70+
validators: zodClient(schema),
71+
resetForm: false,
72+
invalidateAll: false,
73+
async onUpdate(event) {
74+
if (!event.form.valid) {
75+
console.log(event.form.errors)
76+
return
77+
}
78+
79+
await $loginMutation.mutateAsync(event.form.data)
80+
},
81+
},
82+
)
83+
const { enhance, form: formData } = form
84+
85+
let resetPassword = false
86+
</script>
87+
88+
{#if resetPassword}
89+
<ResetPassword />
90+
{:else}
91+
<form method="POST" use:enhance>
92+
<div class="grid gap-4">
93+
<div class="grid gap-2">
94+
<Form.Field {form} name="email">
95+
<Form.Control let:attrs>
96+
<Form.Label for="email">Email</Form.Label>
97+
<Input
98+
{...attrs}
99+
id="email"
100+
type="email"
101+
placeholder="Enter your email to login"
102+
bind:value={$formData.email}
103+
/>
104+
</Form.Control>
105+
<Form.Description />
106+
<Form.FieldErrors />
107+
</Form.Field>
108+
</div>
109+
<div class="grid gap-2">
110+
<Form.Field {form} name="password">
111+
<Form.Control let:attrs>
112+
<div class="flex justify-between">
113+
<Label for="password">Password</Label>
114+
<Button
115+
tabindex={-1}
116+
variant="link"
117+
class="ml-auto h-auto p-0 text-sm"
118+
on:click={() => {
119+
resetPassword = true
120+
}}>Forgot your password?</Button
121+
>
122+
</div>
123+
<PasswordInput {...attrs} id="password" placeholder="*****" bind:value={$formData.password} />
124+
</Form.Control>
125+
<Form.Description />
126+
<Form.FieldErrors />
127+
</Form.Field>
128+
</div>
129+
<Form.Button type="submit" class="w-full" disabled={$loginMutation.isPending}>
130+
{#if $loginMutation.isPending}
131+
<LoaderCircleIcon class="mr-2 h-5 w-5 animate-spin" />
132+
{/if}
133+
Login
134+
</Form.Button>
135+
</div>
136+
<div class="mt-4" use:autoAnimate>
137+
{#if loginError}
138+
<Alert.Root variant="destructive">
139+
<Alert.Title>Error</Alert.Title>
140+
<Alert.Description>Invalid email or password.</Alert.Description>
141+
</Alert.Root>
142+
{/if}
143+
</div>
144+
{#if registrationEnabled}
145+
<div class="mt-4 text-center text-sm">
146+
Don&apos;t have an account?
147+
{#if redirect}
148+
<a href="/signup?redirect={encodeURIComponent(redirect)}" class="underline"> Sign up </a>
149+
{:else}
150+
<a href="/signup" class="underline"> Sign up </a>
151+
{/if}
152+
</div>
153+
{:else}
154+
<p class="text-muted-foreground mt-4 text-center text-xs">
155+
Registration is disabled. <br /> Contact your administrator to request access.
156+
</p>
157+
{/if}
158+
{#if githubEnabled || googleEnabled}
159+
<Separator class="my-6" />
160+
<div class="space-y-2">
161+
{#if githubEnabled}
162+
<Button href="/login/github" variant="secondary" class="w-full">
163+
<img class="mr-2 h-4 w-4" src={Github} alt="github" />
164+
Login with Github
165+
</Button>
166+
{/if}
167+
{#if googleEnabled}
168+
<Button href="/login/google" variant="secondary" class="w-full">
169+
<img class="mr-2 h-4 w-4" src={Google} alt="google" />
170+
Login with Google
171+
</Button>
172+
{/if}
173+
</div>
174+
{/if}
175+
</form>
176+
{/if}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<script lang="ts">
2+
import { goto } from "$app/navigation"
3+
import { Button } from "$lib/components/ui/button/index.js"
4+
import { Input } from "$lib/components/ui/input/index.js"
5+
import { Label } from "$lib/components/ui/label/index.js"
6+
import Logo from "$lib/images/logo.svg"
7+
import Github from "$lib/images/github.svg"
8+
import Google from "$lib/images/Google.svg"
9+
import { createMutation } from "@tanstack/svelte-query"
10+
import { z } from "@undb/zod"
11+
import { defaults, superForm } from "sveltekit-superforms"
12+
import { zodClient } from "sveltekit-superforms/adapters"
13+
import * as Form from "$lib/components/ui/form"
14+
import { Separator } from "$lib/components/ui/separator"
15+
import PasswordInput from "$lib/components/ui/input/password-input.svelte"
16+
import { LoaderCircleIcon } from "lucide-svelte"
17+
18+
export let redirect: string | null
19+
export let invitationId: string | null
20+
export let email: string | null
21+
export let githubEnabled: boolean
22+
export let googleEnabled: boolean
23+
export let onSuccess: () => void = () => {}
24+
25+
const schema = z.object({
26+
email: z.string().email(),
27+
password: z.string().min(6),
28+
username: z.string().min(2).optional(),
29+
})
30+
31+
const formSchema = schema
32+
.merge(
33+
z.object({
34+
confirmPassword: z.string(),
35+
}),
36+
)
37+
.refine((data) => data.password === data.confirmPassword, {
38+
message: "Passwords do not match",
39+
})
40+
41+
type SignupSchema = z.infer<typeof schema>
42+
43+
let signupError = false
44+
45+
const signupMutation = createMutation({
46+
mutationFn: async (input: SignupSchema) => {
47+
try {
48+
const { ok } = await fetch("/api/signup", {
49+
method: "POST",
50+
body: JSON.stringify({ ...input, invitationId }),
51+
})
52+
if (!ok) {
53+
throw new Error("Failed to signup")
54+
}
55+
} catch (error) {
56+
signupError = true
57+
}
58+
},
59+
onMutate(variables) {
60+
signupError = false
61+
},
62+
async onSuccess(data, variables, context) {
63+
onSuccess()
64+
},
65+
onError(error, variables, context) {
66+
signupError = true
67+
},
68+
})
69+
70+
const form = superForm(
71+
defaults(
72+
{
73+
email: email || "",
74+
password: "",
75+
confirmPassword: "",
76+
username: "",
77+
},
78+
zodClient(formSchema),
79+
),
80+
{
81+
SPA: true,
82+
dataType: "json",
83+
validators: zodClient(formSchema),
84+
resetForm: false,
85+
invalidateAll: false,
86+
onUpdate(event) {
87+
if (!event.form.valid) {
88+
console.log(event.form.errors)
89+
return
90+
}
91+
92+
const { confirmPassword, ...data } = event.form.data
93+
$signupMutation.mutate(data)
94+
},
95+
},
96+
)
97+
const { enhance, form: formData, allErrors } = form
98+
$: disabled = $allErrors.length > 0 || $signupMutation.isPending
99+
</script>
100+
101+
<form method="POST" use:enhance>
102+
<div class="grid gap-2">
103+
<div class="grid gap-2">
104+
<Form.Field {form} name="username">
105+
<Form.Control let:attrs>
106+
<Form.Label for="username">Username</Form.Label>
107+
<Input {...attrs} placeholder="Enter your display username" id="username" bind:value={$formData.username} />
108+
</Form.Control>
109+
<Form.Description />
110+
<Form.FieldErrors />
111+
</Form.Field>
112+
</div>
113+
<div class="grid gap-2">
114+
<Form.Field {form} name="email">
115+
<Form.Control let:attrs>
116+
<Form.Label for="email">Email</Form.Label>
117+
<Input {...attrs} id="email" type="email" placeholder="Enter your work email" bind:value={$formData.email} />
118+
</Form.Control>
119+
<Form.Description />
120+
<Form.FieldErrors />
121+
</Form.Field>
122+
</div>
123+
<div class="grid gap-2">
124+
<Form.Field {form} name="password">
125+
<Form.Control let:attrs>
126+
<div class="flex justify-between">
127+
<Label for="password">Password</Label>
128+
</div>
129+
<PasswordInput
130+
{...attrs}
131+
id="password"
132+
type="password"
133+
placeholder="******"
134+
bind:value={$formData.password}
135+
/>
136+
</Form.Control>
137+
<Form.Description />
138+
<Form.FieldErrors />
139+
</Form.Field>
140+
</div>
141+
<div class="grid gap-2">
142+
<Form.Field {form} name="confirmPassword">
143+
<Form.Control let:attrs>
144+
<div class="flex justify-between">
145+
<Label for="confirmPassword">Confirm Password</Label>
146+
</div>
147+
<PasswordInput
148+
{...attrs}
149+
id="confirmPassword"
150+
type="password"
151+
placeholder="******"
152+
bind:value={$formData.confirmPassword}
153+
/>
154+
</Form.Control>
155+
<Form.Description />
156+
<Form.FieldErrors />
157+
</Form.Field>
158+
</div>
159+
<Button {disabled} type="submit" class="w-full">
160+
{#if $signupMutation.isPending}
161+
<LoaderCircleIcon class="mr-2 h-5 w-5 animate-spin" />
162+
{/if}
163+
Create an account
164+
</Button>
165+
</div>
166+
<div class="mt-4 text-center text-sm">
167+
Already have an account?
168+
{#if invitationId}
169+
<a href={`/login?invitationId=${invitationId}`} class="underline"> Sign in </a>
170+
{:else if redirect}
171+
<a href={`/login?redirect=${encodeURIComponent(redirect)}`} class="underline"> Sign in </a>
172+
{:else}
173+
<a href="/login" class="underline"> Sign in </a>
174+
{/if}
175+
</div>
176+
{#if !invitationId && (githubEnabled || googleEnabled)}
177+
<Separator class="my-6" />
178+
<div class="space-y-2">
179+
{#if githubEnabled}
180+
<Button href="/login/github" variant="secondary" class="w-full">
181+
<img class="mr-2 h-4 w-4" src={Github} alt="github" />
182+
Login with Github
183+
</Button>
184+
{/if}
185+
{#if googleEnabled}
186+
<Button href="/login/google" variant="secondary" class="w-full">
187+
<img class="mr-2 h-4 w-4" src={Google} alt="google" />
188+
Login with Google
189+
</Button>
190+
{/if}
191+
</div>
192+
{/if}
193+
</form>

0 commit comments

Comments
 (0)