feat: impl sign up page

This commit is contained in:
2025-12-16 00:48:06 +00:00
parent 3686f87377
commit 45110d05b5
3 changed files with 119 additions and 109 deletions

View File

@@ -1,14 +1,20 @@
import { mutationOptions } from "@tanstack/react-query"
import { type } from "arktype"
import { accountsQuery } from "../account/api"
import { fetchApi } from "../lib/api"
import { currentUserQuery } from "../user/api"
import { User } from "../user/user"
import { Account } from "@/account/account"
import { accountsQuery } from "@/account/api"
import { fetchApi } from "@/lib/api"
import { currentUserQuery } from "@/user/api"
import { User } from "@/user/user"
const LoginResponseSchema = type({
user: User,
})
const SignUpResponse = type({
account: Account,
user: User,
})
export const loginMutation = mutationOptions({
mutationFn: async (data: { email: string; password: string }) => {
const [_, result] = await fetchApi("POST", "/auth/login", {
@@ -25,3 +31,24 @@ export const loginMutation = mutationOptions({
context.client.invalidateQueries(accountsQuery)
},
})
export const signUpMutation = mutationOptions({
mutationFn: async (data: {
displayName: string
email: string
password: string
}) => {
const [_, result] = await fetchApi("POST", "/accounts", {
body: JSON.stringify({
...data,
tokenDelivery: "cookie",
}),
returns: SignUpResponse,
})
return result
},
onSuccess: (data, _, __, context) => {
context.client.setQueryData(currentUserQuery.queryKey, data.user)
context.client.setQueryData(accountsQuery.queryKey, [data.account])
},
})

View File

@@ -165,7 +165,8 @@ function LoginForm() {
{isPending ? "Logging in…" : "Login"}
</Button>
<FieldDescription className="text-center">
Don&apos;t have an account? <a href="#">Sign up</a>
Don&apos;t have an account?{" "}
<a href="/sign-up">Sign up</a>
</FieldDescription>
</Field>
</FieldGroup>

View File

@@ -3,6 +3,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { GalleryVerticalEnd } from "lucide-react"
import type React from "react"
import { toast } from "sonner"
import { signUpMutation } from "@/auth/api"
import { Button } from "@/components/ui/button"
import {
Card,
@@ -14,34 +15,21 @@ import {
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { type AuthErrorCode, authClient, BetterAuthError } from "../auth"
import { cn } from "@/lib/utils"
export const Route = createFileRoute("/sign-up")({
component: SignupPage,
component: RouteComponent,
})
type SignUpParams = {
displayName: string
email: string
password: string
confirmPassword: string
}
class PasswordMismatchError extends Error {
constructor() {
super("Passwords do not match")
}
}
function SignupPage() {
function RouteComponent() {
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-xl flex-col gap-6">
<div className="flex w-full max-w-lg flex-col gap-6">
<a
href="#"
className="flex items-center gap-2 self-center font-medium text-xl"
@@ -51,28 +39,31 @@ function SignupPage() {
</div>
Drexa
</a>
<SignUpFormContainer>
<SignupForm />
</SignUpFormContainer>
<SignupFormCard />
</div>
</div>
)
}
function SignUpFormContainer({ children }: React.PropsWithChildren) {
function SignupFormCard({ className, ...props }: React.ComponentProps<"div">) {
return (
<div className="flex flex-col gap-6">
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">
Create your account
</CardTitle>
<CardTitle className="text-xl">Create an account</CardTitle>
<CardDescription>
Enter your email below to create your account
Enter your information below to create your account
</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
<CardContent>
<SignupForm />
</CardContent>
</Card>
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our{" "}
<a href="#">Terms of Service</a> and{" "}
<a href="#">Privacy Policy</a>.
</FieldDescription>
</div>
)
}
@@ -84,20 +75,7 @@ function SignupForm() {
isPending,
error: signUpError,
} = useMutation({
mutationFn: async (data: SignUpParams) => {
if (data.password !== data.confirmPassword) {
throw new PasswordMismatchError()
}
const { data: signUpData, error } = await authClient.signUp.email({
name: data.displayName,
email: data.email,
password: data.password,
})
if (error) {
throw new BetterAuthError(error.code as AuthErrorCode)
}
return signUpData
},
...signUpMutation,
onSuccess: () => {
navigate({
to: "/",
@@ -117,43 +95,62 @@ function SignupForm() {
displayName: formData.get("displayName") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
confirmPassword: formData.get("confirmPassword") as string,
})
}
let passwordFieldError = null
let emailFieldError = null
if (signUpError instanceof BetterAuthError) {
switch (signUpError.errorCode) {
case "PASSWORD_TOO_SHORT":
passwordFieldError =
"Password must be at least 8 characters long"
break
case "INVALID_EMAIL":
emailFieldError = "Invalid email address"
break
default:
passwordFieldError = null
}
} else if (signUpError instanceof PasswordMismatchError) {
passwordFieldError = "Passwords do not match"
}
return (
<form onSubmit={handleSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Your Name</FieldLabel>
<Button
disabled={isPending}
variant="outline"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<title>Apple logo</title>
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
Sign up with Apple
</Button>
<Button
disabled={isPending}
variant="outline"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<title>Google logo</title>
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Sign up with Google
</Button>
</Field>
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
Or continue with
</FieldSeparator>
<Field>
<FieldLabel htmlFor="name">Display Name</FieldLabel>
<Input
disabled={isPending}
name="displayName"
type="text"
placeholder="John Doe"
required
/>
<FieldDescription>
This is how you will be referred to on Drexa. You can
change this later.
This will be displayed in the app and to other users.
Can be any name of your choice.
</FieldDescription>
</Field>
<Field>
@@ -165,12 +162,7 @@ function SignupForm() {
placeholder="m@example.com"
required
/>
{emailFieldError ? (
<FieldError>{emailFieldError}</FieldError>
) : null}
</Field>
<Field>
<Field className="grid grid-rows-2 md:grid-rows-1 md:grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input
@@ -179,6 +171,9 @@ function SignupForm() {
type="password"
required
/>
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
@@ -191,25 +186,12 @@ function SignupForm() {
required
/>
</Field>
</Field>
{passwordFieldError ? (
<FieldError>{passwordFieldError}</FieldError>
) : (
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
)}
</Field>
<Field>
<Button
type="submit"
loading={isPending}
disabled={isPending}
>
<Button disabled={isPending} type="submit">
{isPending ? "Creating account…" : "Create Account"}
</Button>
<FieldDescription className="text-center">
Already have an account? <a href="/sign-in">Sign in</a>
Already have an account? <a href="/login">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>