Skip to content

Commit 6859bed

Browse files
committed
Setup the layout and components and remix hook form
1 parent 0fc232e commit 6859bed

File tree

13 files changed

+586
-3
lines changed

13 files changed

+586
-3
lines changed

app/components/ui/button.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Slot } from "@radix-ui/react-slot"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
import type * as React from "react"
4+
5+
import { cn } from "~/lib/utils"
6+
7+
const buttonVariants = cva(
8+
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
13+
destructive:
14+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
15+
outline:
16+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
17+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
18+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
19+
link: "text-primary underline-offset-4 hover:underline",
20+
},
21+
size: {
22+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
23+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
24+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
25+
icon: "size-9",
26+
},
27+
},
28+
defaultVariants: {
29+
variant: "default",
30+
size: "default",
31+
},
32+
}
33+
)
34+
35+
function Button({
36+
className,
37+
variant,
38+
size,
39+
asChild = false,
40+
...props
41+
}: React.ComponentProps<"button"> &
42+
VariantProps<typeof buttonVariants> & {
43+
asChild?: boolean
44+
}) {
45+
const Comp = asChild ? Slot : "button"
46+
47+
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
48+
}
49+
50+
export { Button }

app/components/ui/input.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type * as React from "react"
2+
3+
import { cn } from "~/lib/utils"
4+
5+
export function InputField({ children }: React.PropsWithChildren) {
6+
return <div className="flex w-full flex-col gap-1">{children}</div>
7+
}
8+
9+
export function Input({
10+
className,
11+
type,
12+
error,
13+
...props
14+
}: React.ComponentProps<"input"> & {
15+
error?: boolean
16+
}) {
17+
return (
18+
<input
19+
type={type}
20+
data-slot="input"
21+
className={cn(
22+
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
23+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
24+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
25+
error &&
26+
"border-destructive aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
27+
className
28+
)}
29+
{...props}
30+
/>
31+
)
32+
}
33+
34+
export function InputError({ children }: React.PropsWithChildren) {
35+
return (
36+
<p className="mt-0 w-full text-left text-destructive text-sm" role="alert">
37+
{children}
38+
</p>
39+
)
40+
}

app/lib/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type ClassValue, clsx } from "clsx"
2+
import { twMerge } from "tailwind-merge"
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs))
6+
}

app/root.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ export default function App({ loaderData }: Route.ComponentProps) {
3535
export const Layout = ({ children }: { children: React.ReactNode }) => {
3636
const { i18n } = useTranslation()
3737
return (
38-
<html className="overflow-y-auto overflow-x-hidden" lang={i18n.language} dir={i18n.dir()}>
38+
<html className="h-full overflow-y-auto overflow-x-hidden" lang={i18n.language} dir={i18n.dir()}>
3939
<head>
4040
<ClientHintCheck />
4141
<meta charSet="utf-8" />
4242
<meta name="viewport" content="width=device-width, initial-scale=1" />
4343
<Meta />
4444
<Links />
4545
</head>
46-
<body className="h-full w-full">
46+
<body className="h-full w-full overflow-y-auto">
4747
<LanguageSwitcher />
4848
{children}
4949
<ScrollRestoration />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { href } from "react-router"
2+
import { Button } from "~/components/ui/button"
3+
import { Input } from "~/components/ui/input"
4+
import { Link } from "~/library/link"
5+
6+
export default function ForgotPasswordRoute() {
7+
return (
8+
<div className="mx-auto flex h-full w-full flex-col items-center justify-center gap-4 lg:w-2/3">
9+
<h1 className="mb-2 text-center text-6xl text-black lg:mb-4">Forgot password</h1>
10+
<p className="text-center">Forgot password description</p>
11+
<Input className="w-full" placeholder="Enter your email" name="email" />
12+
<Link viewTransition={false} to={href("/login")} />
13+
<Button size="lg">Send password reset email</Button>
14+
</div>
15+
)
16+
}

app/routes/_auth.login.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { zodResolver } from "@hookform/resolvers/zod"
2+
import { Form, href } from "react-router"
3+
import { getValidatedFormData, useRemixForm } from "remix-hook-form"
4+
import { z } from "zod/v4"
5+
import { Button } from "~/components/ui/button"
6+
import { Input, InputError, InputField } from "~/components/ui/input"
7+
import { Link } from "~/library/link"
8+
import type { Route } from "./+types/_auth.login"
9+
10+
const loginFormSchema = z.object({
11+
email: z.email(),
12+
password: z.string().min(8).max(100),
13+
redirectTo: z.string().optional(),
14+
})
15+
16+
const resolver = zodResolver(loginFormSchema)
17+
18+
export const action = async ({ request }: Route.ActionArgs) => {
19+
const { errors } = await getValidatedFormData(request, resolver)
20+
if (errors) {
21+
return { errors }
22+
}
23+
// your implementation here
24+
return null
25+
}
26+
27+
export default function LoginRoute() {
28+
const { handleSubmit, register, formState } = useRemixForm({
29+
resolver,
30+
})
31+
return (
32+
<Form onSubmit={handleSubmit} className="flex h-full items-center justify-center" method="post">
33+
<div className="mx-auto flex h-full w-full flex-col items-center justify-center gap-4 lg:w-2/3">
34+
<h1 className="mb-2 text-center text-6xl text-black lg:mb-4">Login</h1>
35+
<p className="text-center">Login below</p>
36+
<InputField>
37+
<Input
38+
{...register("email")}
39+
error={!!formState.errors.email}
40+
placeholder="Enter your email"
41+
autoFocus
42+
className="w-full"
43+
/>
44+
<InputError>{formState.errors.email?.message}</InputError>
45+
</InputField>
46+
<InputField>
47+
<Input
48+
{...register("password")}
49+
error={!!formState.errors.password}
50+
placeholder="Enter your password"
51+
className="w-full"
52+
type="password"
53+
/>
54+
<InputError>{formState.errors.password?.message}</InputError>
55+
</InputField>
56+
57+
<Link viewTransition={false} to={href("/forgot-password")}>
58+
Forgot password?
59+
</Link>
60+
<Button type="submit" size="lg">
61+
Login
62+
</Button>
63+
</div>
64+
</Form>
65+
)
66+
}

app/routes/_auth.register.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { zodResolver } from "@hookform/resolvers/zod"
2+
import { Form } from "react-router"
3+
import { getValidatedFormData, useRemixForm } from "remix-hook-form"
4+
import { z } from "zod/v4"
5+
import { Button } from "~/components/ui/button"
6+
import { Input, InputError, InputField } from "~/components/ui/input"
7+
import type { Route } from "./+types/_auth.register"
8+
9+
const registerFormSchema = z
10+
.object({
11+
email: z.email(),
12+
password: z.string().min(8).max(100),
13+
confirmPassword: z.string().min(8).max(100),
14+
})
15+
.refine((data) => data.password === data.confirmPassword, {
16+
message: "Passwords don't match",
17+
path: ["confirmPassword"],
18+
})
19+
20+
const resolver = zodResolver(registerFormSchema)
21+
22+
export const action = async ({ request }: Route.ActionArgs) => {
23+
const { errors } = await getValidatedFormData(request, resolver)
24+
if (errors) {
25+
return { errors }
26+
}
27+
return null
28+
}
29+
30+
export default function RegisterRoute() {
31+
const { handleSubmit, register, formState } = useRemixForm({
32+
resolver,
33+
})
34+
return (
35+
<Form onSubmit={handleSubmit} className="mx-auto flex h-full w-full items-center justify-center" method="post">
36+
<div className="mx-auto flex h-full w-full flex-col items-center justify-center gap-4 lg:w-2/3">
37+
<h1 className="mb-2 text-center text-6xl text-black lg:mb-4">Register</h1>
38+
<p className="text-center">Register below</p>
39+
<InputField>
40+
<Input
41+
{...register("email")}
42+
error={!!formState.errors.email}
43+
placeholder="Enter your email"
44+
autoFocus
45+
className="w-full"
46+
/>
47+
<InputError>{formState.errors.email?.message}</InputError>
48+
</InputField>
49+
<InputField>
50+
<Input
51+
{...register("password")}
52+
error={!!formState.errors.password}
53+
placeholder="Enter your password"
54+
className="w-full"
55+
type="password"
56+
/>
57+
<InputError>{formState.errors.password?.message}</InputError>
58+
</InputField>
59+
<InputField>
60+
<Input
61+
{...register("confirmPassword")}
62+
error={!!formState.errors.confirmPassword}
63+
placeholder="Confirm password"
64+
type="password"
65+
className="w-full"
66+
/>
67+
<InputError>{formState.errors.confirmPassword?.message}</InputError>
68+
</InputField>
69+
70+
<Button type="submit" size="lg">
71+
Register
72+
</Button>
73+
</div>
74+
</Form>
75+
)
76+
}

app/routes/_auth.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { href, Outlet, useLocation } from "react-router"
2+
import { Button } from "~/components/ui/button"
3+
import { Link } from "~/library/link"
4+
import { cn } from "~/utils/css"
5+
6+
const useGetCurrentPage = () => {
7+
// Used to retrieve the url
8+
const location = useLocation()
9+
// Gets the current path name (url)
10+
const url = location.pathname
11+
return {
12+
// Ends with login? We are on the login page
13+
isLoginPage: url.endsWith("/login"),
14+
// Ends with register? We are on the register page
15+
isRegisterPage: url.endsWith("/register"),
16+
// Ends with forgot password? we are on the forgot password page
17+
isForgotPasswordPage: url.endsWith("/forgot-password"),
18+
}
19+
}
20+
export default function LoginLayout() {
21+
const { isForgotPasswordPage, isLoginPage, isRegisterPage } = useGetCurrentPage()
22+
23+
const key = isLoginPage ? "Login" : "Register"
24+
return (
25+
<div className="relative z-10 flex min-h-screen w-full items-start justify-center overflow-hidden md:items-center">
26+
<div className="relative z-10 flex h-screen w-full flex-col-reverse bg-white drop-shadow-2xl md:h-[75vh] md:w-11/12 md:flex-row lg:w-2/3">
27+
<div
28+
className={cn(
29+
// Color of the box, add what you want!
30+
"bg-gradient-to-br from-10% from-indigo-500 via-30% via-sky-500 to-90% to-emerald-500",
31+
"z-20 flex h-full w-full origin-left scale-x-100 flex-col items-center justify-center p-4 px-8 transition-all md:w-1/2 lg:px-20",
32+
// On register page this box will be on the right side
33+
isRegisterPage && "md:translate-x-full",
34+
// On forgot password page this block will be hidden
35+
isForgotPasswordPage && " scale-x-0"
36+
)}
37+
>
38+
<div className="flex flex-col items-center gap-4">
39+
<h1 className="!text-6xl text-center text-black">{key} Title</h1>
40+
<p className="font-semibold text-black">{key} Description</p>
41+
42+
<Link to={isLoginPage ? href("/register") : href("/login")}>
43+
<Button>{key === "Login" ? "Register" : "Login"}</Button>
44+
</Link>
45+
</div>
46+
</div>
47+
48+
<div
49+
className={cn(
50+
"z-10 w-full p-8 transition-transform md:w-1/2 lg:p-0",
51+
isRegisterPage && "md:-translate-x-full",
52+
isForgotPasswordPage && "-translate-x-1/2"
53+
)}
54+
>
55+
<Outlet />
56+
</div>
57+
</div>
58+
</div>
59+
)
60+
}

0 commit comments

Comments
 (0)