mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-02 14:41:18 +00:00
feat: impl sign up page
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
import { mutationOptions } from "@tanstack/react-query"
|
import { mutationOptions } from "@tanstack/react-query"
|
||||||
import { type } from "arktype"
|
import { type } from "arktype"
|
||||||
import { accountsQuery } from "../account/api"
|
import { Account } from "@/account/account"
|
||||||
import { fetchApi } from "../lib/api"
|
import { accountsQuery } from "@/account/api"
|
||||||
import { currentUserQuery } from "../user/api"
|
import { fetchApi } from "@/lib/api"
|
||||||
import { User } from "../user/user"
|
import { currentUserQuery } from "@/user/api"
|
||||||
|
import { User } from "@/user/user"
|
||||||
|
|
||||||
const LoginResponseSchema = type({
|
const LoginResponseSchema = type({
|
||||||
user: User,
|
user: User,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const SignUpResponse = type({
|
||||||
|
account: Account,
|
||||||
|
user: User,
|
||||||
|
})
|
||||||
|
|
||||||
export const loginMutation = mutationOptions({
|
export const loginMutation = mutationOptions({
|
||||||
mutationFn: async (data: { email: string; password: string }) => {
|
mutationFn: async (data: { email: string; password: string }) => {
|
||||||
const [_, result] = await fetchApi("POST", "/auth/login", {
|
const [_, result] = await fetchApi("POST", "/auth/login", {
|
||||||
@@ -25,3 +31,24 @@ export const loginMutation = mutationOptions({
|
|||||||
context.client.invalidateQueries(accountsQuery)
|
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])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ function LoginForm() {
|
|||||||
{isPending ? "Logging in…" : "Login"}
|
{isPending ? "Logging in…" : "Login"}
|
||||||
</Button>
|
</Button>
|
||||||
<FieldDescription className="text-center">
|
<FieldDescription className="text-center">
|
||||||
Don't have an account? <a href="#">Sign up</a>
|
Don't have an account?{" "}
|
||||||
|
<a href="/sign-up">Sign up</a>
|
||||||
</FieldDescription>
|
</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
|||||||
import { GalleryVerticalEnd } from "lucide-react"
|
import { GalleryVerticalEnd } from "lucide-react"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { signUpMutation } from "@/auth/api"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -14,34 +15,21 @@ import {
|
|||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
FieldGroup,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
|
FieldSeparator,
|
||||||
} from "@/components/ui/field"
|
} from "@/components/ui/field"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { type AuthErrorCode, authClient, BetterAuthError } from "../auth"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export const Route = createFileRoute("/sign-up")({
|
export const Route = createFileRoute("/sign-up")({
|
||||||
component: SignupPage,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
type SignUpParams = {
|
function RouteComponent() {
|
||||||
displayName: string
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
confirmPassword: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class PasswordMismatchError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Passwords do not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignupPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
<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
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="flex items-center gap-2 self-center font-medium text-xl"
|
className="flex items-center gap-2 self-center font-medium text-xl"
|
||||||
@@ -51,28 +39,31 @@ function SignupPage() {
|
|||||||
</div>
|
</div>
|
||||||
Drexa
|
Drexa
|
||||||
</a>
|
</a>
|
||||||
<SignUpFormContainer>
|
<SignupFormCard />
|
||||||
<SignupForm />
|
|
||||||
</SignUpFormContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SignUpFormContainer({ children }: React.PropsWithChildren) {
|
function SignupFormCard({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">Create an account</CardTitle>
|
||||||
Create your account
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter your email below to create your account
|
Enter your information below to create your account
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>{children}</CardContent>
|
<CardContent>
|
||||||
|
<SignupForm />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,20 +75,7 @@ function SignupForm() {
|
|||||||
isPending,
|
isPending,
|
||||||
error: signUpError,
|
error: signUpError,
|
||||||
} = useMutation({
|
} = useMutation({
|
||||||
mutationFn: async (data: SignUpParams) => {
|
...signUpMutation,
|
||||||
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
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/",
|
to: "/",
|
||||||
@@ -117,43 +95,62 @@ function SignupForm() {
|
|||||||
displayName: formData.get("displayName") as string,
|
displayName: formData.get("displayName") as string,
|
||||||
email: formData.get("email") as string,
|
email: formData.get("email") as string,
|
||||||
password: formData.get("password") 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 (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field>
|
<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
|
<Input
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
name="displayName"
|
name="displayName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="John Doe"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<FieldDescription>
|
<FieldDescription>
|
||||||
This is how you will be referred to on Drexa. You can
|
This will be displayed in the app and to other users.
|
||||||
change this later.
|
Can be any name of your choice.
|
||||||
</FieldDescription>
|
</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
@@ -165,12 +162,7 @@ function SignupForm() {
|
|||||||
placeholder="m@example.com"
|
placeholder="m@example.com"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{emailFieldError ? (
|
|
||||||
<FieldError>{emailFieldError}</FieldError>
|
|
||||||
) : null}
|
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
|
||||||
<Field className="grid grid-rows-2 md:grid-rows-1 md:grid-cols-2 gap-4">
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -179,6 +171,9 @@ function SignupForm() {
|
|||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<FieldDescription>
|
||||||
|
Must be at least 8 characters long.
|
||||||
|
</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="confirm-password">
|
<FieldLabel htmlFor="confirm-password">
|
||||||
@@ -191,25 +186,12 @@ function SignupForm() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Field>
|
|
||||||
{passwordFieldError ? (
|
|
||||||
<FieldError>{passwordFieldError}</FieldError>
|
|
||||||
) : (
|
|
||||||
<FieldDescription>
|
|
||||||
Must be at least 8 characters long.
|
|
||||||
</FieldDescription>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
<Field>
|
||||||
<Button
|
<Button disabled={isPending} type="submit">
|
||||||
type="submit"
|
|
||||||
loading={isPending}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
{isPending ? "Creating account…" : "Create Account"}
|
{isPending ? "Creating account…" : "Create Account"}
|
||||||
</Button>
|
</Button>
|
||||||
<FieldDescription className="text-center">
|
<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>
|
</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user