Made login / register UI. Started implementing validation

This commit is contained in:
Leons Aleksandrovs
2025-07-04 22:39:00 +03:00
parent 2cd3027b72
commit 58d53b17cb
11 changed files with 273 additions and 66 deletions

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,3 @@
export default function Guest({ children, className = "" }: React.ComponentProps<"div">) {
return <div className={`${className}`}>{children}</div>;
}

View File

@@ -9,9 +9,15 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as RegisterRouteImport } from './routes/register'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
const RegisterRoute = RegisterRouteImport.update({
id: '/register',
path: '/register',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
@@ -26,31 +32,42 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login'
fullPaths: '/' | '/login' | '/register'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/login'
id: '__root__' | '/' | '/login'
to: '/' | '/login' | '/register'
id: '__root__' | '/' | '/login' | '/register'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/register': {
id: '/register'
path: '/register'
fullPath: '/register'
preLoaderRoute: typeof RegisterRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
id: '/login'
path: '/login'
@@ -71,6 +88,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -1,9 +1,32 @@
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import Guest from "@/layouts/Guest";
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/login"!</div>;
return (
<Guest className="h-screen w-screen grid place-items-center">
<Card className="w-full max-w-[400px]">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Log into your account</CardDescription>
<CardAction>
<Button variant={"link"}>
<Link to="/register">Register</Link>
</Button>
</CardAction>
</CardHeader>
<CardContent className="space-y-2">
<Input type="email" placeholder="Email address" />
<Input type="password" placeholder="Password" />
<Button className="w-full">Login</Button>
</CardContent>
</Card>
</Guest>
);
}

View File

@@ -0,0 +1,75 @@
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import Guest from "@/layouts/Guest";
import { useForm } from "@tanstack/react-form";
import { createFileRoute, Link } from "@tanstack/react-router";
import * as z from "zod/v4";
export const Route = createFileRoute("/register")({
component: RouteComponent,
});
const registerSchema = z
.object({
email: z.string().email(),
password: z.string().min(8),
repeatPassword: z.string().min(8),
})
.refine((data) => data.password === data.repeatPassword, {
message: "Passwords don't match",
path: ["repeatPassword"],
});
function RouteComponent() {
const form = useForm({
defaultValues: {
email: "",
password: "",
repeatPassword: "",
},
validators: {
onBlur: registerSchema,
},
onSubmit: ({ value }) => {
console.log(value);
},
});
return (
<Guest className="h-screen w-screen grid place-items-center">
<Card className="w-full max-w-[400px]">
<CardHeader>
<CardTitle>Register</CardTitle>
<CardDescription>Create an account</CardDescription>
<CardAction>
<Button variant={"link"}>
<Link to="/login">Login</Link>
</Button>
</CardAction>
</CardHeader>
<CardContent className="space-y-2">
<form.Field
name="email"
children={(field) => (
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
placeholder="Email address"
/>
)}
/>
<Input type="password" placeholder="Password" />
<Input type="password" placeholder="Repeat password" />
<Button onClick={form.handleSubmit} className="w-full">
Register
</Button>
</CardContent>
</Card>
</Guest>
);
}

View File

@@ -3,62 +3,6 @@
@custom-variant dark (&:is(.dark *));
@theme {
--color-body: rgb(230, 230, 230);
--color-panel: rgb(250, 250, 250);
}
body {
@apply m-0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
@@ -128,6 +72,48 @@ code {
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* My own variables */
--color-body: var(--background);
--color-panel: var(--card);
}
@layer base {
* {
@apply border-border outline-ring/50;
@@ -136,3 +122,16 @@ code {
@apply bg-background text-foreground;
}
}
body {
@apply m-0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}

View File

@@ -1,5 +0,0 @@
#!/bin/bash
file=development.yml
docker compose -f $file down && docker compose -f $file up --build -d

6
scripts/rebuild.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Load variables
source ./var.sh
cd .. && docker compose -f $file down && docker compose -f $file up --build -d

6
scripts/stop.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Load variables
source ./var.sh
cd .. && docker compose -f $file down

2
scripts/var.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
export file="development.yml"