feat(frontend): finished login form with validation

This commit is contained in:
2025-02-21 09:16:07 -06:00
parent d939332499
commit 9719451580
25 changed files with 551 additions and 230 deletions

View File

@@ -1,4 +1,4 @@
import {createRootRoute, Outlet} from "@tanstack/react-router";
import {createRootRoute, Link, Outlet} from "@tanstack/react-router";
import {TanStackRouterDevtools} from "@tanstack/router-devtools";
import Cookies from "js-cookie";
import {SidebarProvider} from "../components/ui/sidebar";
@@ -14,48 +14,74 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu";
import {SessionProvider} from "../components/providers/Providers";
import {Toaster} from "sonner";
import {Button} from "../components/ui/button";
import {useLogout} from "../lib/hooks/useLogout";
import {useSession} from "../lib/hooks/useSession";
import {useSessionStore} from "../lib/store/sessionStore";
// same as the layout
export const Route = createRootRoute({
component: () => {
const sidebarState = Cookies.get("sidebar_state") === "true";
const {session} = useSession();
const {user} = useSessionStore();
const logout = useLogout();
return (
<>
<ThemeProvider>
<nav className="flex justify-end">
<div className="m-2 flex flex-row">
<div className="m-auto pr-2">
<p>Add Card</p>
<SessionProvider>
<ThemeProvider>
<nav className="flex justify-end">
<div className="m-2 flex flex-row">
<div className="m-auto pr-2">
<p>Add Card</p>
</div>
<div className="m-1">
<ModeToggle />
</div>
{session ? (
<div className="m-1">
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Hello {user?.username}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
<hr className="solid"></hr>
<DropdownMenuItem>
<div className="m-auto mt-3">
<Button onClick={() => logout()} variant="ghost">
Logout
</Button>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<>
<Link to="/login">Login</Link>
</>
)}
</div>
<div className="m-1">
<ModeToggle />
</div>
<div className="m-1">
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</nav>
<SidebarProvider defaultOpen={sidebarState}>
<AppSidebar />
<Outlet />
</SidebarProvider>
</ThemeProvider>
</nav>
<SidebarProvider defaultOpen={sidebarState}>
<AppSidebar />
<Toaster expand={true} richColors closeButton />
<Outlet />
</SidebarProvider>
</ThemeProvider>
</SessionProvider>
<TanStackRouterDevtools position="bottom-right" />
</>

View File

@@ -0,0 +1,13 @@
import {createFileRoute, redirect} from "@tanstack/react-router";
// src/routes/_authenticated.tsx
export const Route = createFileRoute("/_admin")({
beforeLoad: async () => {
const auth = localStorage.getItem("auth_token");
if (!auth) {
throw redirect({
to: "/login",
});
}
},
});

View File

@@ -6,7 +6,7 @@ export const Route = createFileRoute("/_auth")({
const auth = localStorage.getItem("auth_token");
if (!auth) {
throw redirect({
to: "/",
to: "/login",
});
}
},

View File

@@ -1,38 +1,143 @@
import {createFileRoute, useRouter} from "@tanstack/react-router";
import {isAuthenticated, signIn, signOut} from "../utils/auth";
import {createFileRoute, redirect, useRouter} from "@tanstack/react-router";
import {useForm, Controller} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {LstCard} from "../components/extendedUI/LstCard";
import {Button} from "../components/ui/button";
import {Input} from "../components/ui/input";
import {CardHeader} from "../components/ui/card";
import {Label} from "../components/ui/label";
import {toast} from "sonner";
import {useSessionStore} from "../lib/store/sessionStore";
import {Checkbox} from "../components/ui/checkbox";
import {z} from "zod";
export const Route = createFileRoute("/login")({
component: RouteComponent,
beforeLoad: () => {
const isLoggedIn = localStorage.getItem("auth_token");
if (isLoggedIn) {
throw redirect({
to: "/",
});
}
},
});
const FormSchema = z.object({
username: z.string().min(1, "You must enter a valid username"),
password: z.string().min(4, "You must enter a valid password"),
rememberMe: z.boolean(),
});
function RouteComponent() {
const {setSession} = useSessionStore();
const rememeberMe = localStorage.getItem("rememberMe") === "true";
const username = localStorage.getItem("username") || "";
const router = useRouter();
const {
register,
handleSubmit,
control,
formState: {errors},
} = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
username: username || "",
password: "",
rememberMe: rememeberMe,
},
});
const onSubmitLogin = async (value: z.infer<typeof FormSchema>) => {
// Do something with form data
// first update the rememberMe incase it was selected
if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("username", value.username);
} else {
localStorage.removeItem("rememberMe");
localStorage.removeItem("username");
}
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({username: value.username, password: value.password}),
});
// if (!response.ok) {
// throw new Error("Invalid credentials");
// }
const data = await response.json();
// Store token in localStorage
// localStorage.setItem("auth_token", data.data.token);
setSession(data.data.user, data.data.token);
toast.success(`You are logged in as ${data.data.user.username}`);
router.navigate({to: "/"});
} catch (err) {
toast.error("Invalid credentials");
}
};
return (
<div>
<h2>Ligin</h2>
{isAuthenticated() ? (
<>
<p>Hello User!</p>
<Button
onClick={async () => {
signOut();
router.invalidate();
}}
>
signOut
</Button>
</>
) : (
<Button
onClick={async () => {
signIn();
router.invalidate();
}}
>
Sign in
</Button>
)}
<div className="ml-[25%]">
<LstCard className="p-3 w-96">
<CardHeader>
<div>
<p className="text-2xl">Login to LST</p>
</div>
</CardHeader>
<hr className="rounded"></hr>
<form onSubmit={handleSubmit(onSubmitLogin)}>
<div>
<Label>Username</Label>
<Input
placeholder="smith001"
{...register("username")}
className={errors.username ? "border-red-500" : ""}
aria-invalid={!!errors.username}
/>
{errors.username && <p className="text-red-500 text-sm mt-1">{errors.username.message}</p>}
</div>
<div>
<>
<Label htmlFor={"password"}>Password</Label>
<Input
type="password"
{...register("password")}
className={errors.password ? "border-red-500" : ""}
aria-invalid={!!errors.password}
/>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
</>
</div>
<div className="flex justify-between pt-2">
<div className="flex">
<Controller
render={({field}) => (
<Checkbox id="remember" checked={field.value} onCheckedChange={field.onChange} />
)}
control={control}
name="rememberMe"
defaultValue={rememeberMe}
/>
<Label htmlFor="remember" className="pl-2">
remember me
</Label>
</div>
<div className="flex justify-end">
<Button type="submit">Submit</Button>
</div>
</div>
</form>
</LstCard>
</div>
);
}