style(auth): redesign login and onboarding pages

This commit is contained in:
Nicolas Meienberger
2025-10-25 17:05:20 +02:00
parent d58c4f793d
commit 47ff720adb
7 changed files with 145 additions and 121 deletions

View File

@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "dither-plugin";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));

View File

@@ -0,0 +1,34 @@
import { Mountain } from "lucide-react";
import type { ReactNode } from "react";
type AuthLayoutProps = {
title: string;
description: string;
children: ReactNode;
};
export function AuthLayout({ title, description, children }: AuthLayoutProps) {
return (
<div className="flex min-h-screen">
<div className="flex flex-1 items-center justify-center bg-background p-8">
<div className="w-full max-w-md space-y-8">
<div className="flex items-center gap-3">
<Mountain className="size-5 text-strong-accent" />
<span className="text-lg font-semibold">Ironmount</span>
</div>
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
{children}
</div>
</div>
<div
className="hidden lg:block lg:flex-1 dither-xl bg-cover bg-center"
style={{ backgroundImage: "url(/background.jpg)" }}
/>
</div>
);
}

View File

@@ -5,9 +5,8 @@ import { useForm } from "react-hook-form";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { loginMutation } from "~/api-client/@tanstack/react-query.gen"; import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
import { GridBackground } from "~/components/grid-background"; import { AuthLayout } from "~/components/auth-layout";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
@@ -50,54 +49,49 @@ export default function LoginPage() {
}; };
return ( return (
<GridBackground className="flex items-center justify-center p-4"> <AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
<Card className="w-full max-w-md"> <Form {...form}>
<CardHeader> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle> <FormField
<CardDescription>Sign in to your account</CardDescription> control={form.control}
</CardHeader> name="username"
<CardContent> render={({ field }) => (
<Form {...form}> <FormItem>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormLabel>Username</FormLabel>
<FormField <FormControl>
control={form.control} <Input {...field} type="text" placeholder="admin" disabled={login.isPending} autoFocus />
name="username" </FormControl>
render={({ field }) => ( <FormMessage />
<FormItem> </FormItem>
<FormLabel>Username</FormLabel> )}
<FormControl> />
<Input <FormField
{...field} control={form.control}
type="text" name="password"
placeholder="Enter your username" render={({ field }) => (
disabled={login.isPending} <FormItem>
autoFocus <div className="flex items-center justify-between">
/> <FormLabel>Password</FormLabel>
</FormControl> <button
<FormMessage /> type="button"
</FormItem> className="text-xs text-muted-foreground hover:underline"
)} onClick={() => toast.info("Password reset not implemented")}
/> >
<FormField Forgot your password?
control={form.control} </button>
name="password" </div>
render={({ field }) => ( <FormControl>
<FormItem> <Input {...field} type="password" disabled={login.isPending} />
<FormLabel>Password</FormLabel> </FormControl>
<FormControl> <FormMessage />
<Input {...field} type="password" placeholder="Enter your password" disabled={login.isPending} /> </FormItem>
</FormControl> )}
<FormMessage /> />
</FormItem> <Button type="submit" className="w-full" loading={login.isPending}>
)} Login
/> </Button>
<Button type="submit" className="w-full" loading={login.isPending}> </form>
Sign In </Form>
</Button> </AuthLayout>
</form>
</Form>
</CardContent>
</Card>
</GridBackground>
); );
} }

View File

@@ -5,9 +5,8 @@ import { useForm } from "react-hook-form";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { registerMutation } from "~/api-client/@tanstack/react-query.gen"; import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
import { GridBackground } from "~/components/grid-background"; import { AuthLayout } from "~/components/auth-layout";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
@@ -61,73 +60,65 @@ export default function OnboardingPage() {
}; };
return ( return (
<GridBackground className="flex items-center justify-center p-4"> <AuthLayout title="Welcome to Ironmount" description="Create the admin user to get started">
<Card className="w-full max-w-md"> <Form {...form}>
<CardHeader> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle> <FormField
<CardDescription>Create the admin user to get started</CardDescription> control={form.control}
</CardHeader> name="username"
<CardContent> render={({ field }) => (
<Form {...form}> <FormItem>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormLabel>Username</FormLabel>
<FormField <FormControl>
control={form.control} <Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
name="username" </FormControl>
render={({ field }) => ( <FormDescription>Choose a username for the admin account</FormDescription>
<FormItem> <FormMessage />
<FormLabel>Username</FormLabel> </FormItem>
<FormControl> )}
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus /> />
</FormControl> <FormField
<FormDescription>Choose a username for the admin account</FormDescription> control={form.control}
<FormMessage /> name="password"
</FormItem> render={({ field }) => (
)} <FormItem>
/> <FormLabel>Password</FormLabel>
<FormField <FormControl>
control={form.control} <Input
name="password" {...field}
render={({ field }) => ( type="password"
<FormItem> placeholder="Enter a secure password"
<FormLabel>Password</FormLabel> disabled={registerUser.isPending}
<FormControl> />
<Input </FormControl>
{...field} <FormDescription>Password must be at least 8 characters long.</FormDescription>
type="password" <FormMessage />
placeholder="Enter a secure password" </FormItem>
disabled={registerUser.isPending} )}
/> />
</FormControl> <FormField
<FormDescription>Password must be at least 8 characters long.</FormDescription> control={form.control}
<FormMessage /> name="confirmPassword"
</FormItem> render={({ field }) => (
)} <FormItem>
/> <FormLabel>Confirm Password</FormLabel>
<FormField <FormControl>
control={form.control} <Input
name="confirmPassword" {...field}
render={({ field }) => ( type="password"
<FormItem> placeholder="Re-enter your password"
<FormLabel>Confirm Password</FormLabel> disabled={registerUser.isPending}
<FormControl> />
<Input </FormControl>
{...field} <FormMessage />
type="password" </FormItem>
placeholder="Re-enter your password" )}
disabled={registerUser.isPending} />
/> <Button type="submit" className="w-full" loading={registerUser.isPending}>
</FormControl> Create Admin User
<FormMessage /> </Button>
</FormItem> </form>
)} </Form>
/> </AuthLayout>
<Button type="submit" className="w-full" loading={registerUser.isPending}>
Create Admin User
</Button>
</form>
</Form>
</CardContent>
</Card>
</GridBackground>
); );
} }

View File

@@ -30,6 +30,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dither-plugin": "^1.1.1",
"isbot": "^5.1.31", "isbot": "^5.1.31",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -32,6 +32,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dither-plugin": "^1.1.1",
"isbot": "^5.1.31", "isbot": "^5.1.31",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@@ -720,6 +721,8 @@
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"dither-plugin": ["dither-plugin@1.1.1", "", {}, "sha512-PsgAcSoNVKkwh+Q/OopRn/2qb9HW1LRyGqT1bQe8iooYvVY1FIIqePFN9JkEIVK9rkfZdj7nXn9EUB4B7mNh6g=="],
"docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="],
"dockerode": ["dockerode@4.0.9", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "^2.1.4", "uuid": "^10.0.0" } }, "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q=="], "dockerode": ["dockerode@4.0.9", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "^2.1.4", "uuid": "^10.0.0" } }, "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q=="],