login form created
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/lst.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Logistics Support Tool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,37 +1,113 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import { SidebarIcon } from "lucide-react";
|
||||
import { authClient, useSession } from "@/lib/auth-client";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu";
|
||||
import { useSidebar } from "./ui/sidebar";
|
||||
|
||||
export default function Header() {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { data: session } = useSession();
|
||||
const { signOut } = authClient;
|
||||
const router = useRouterState();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = router.location.href;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 flex w-full items-center border-b bg-background">
|
||||
<div className="flex h-(--header-height) w-full items-center gap-2 px-4">
|
||||
<div className="flex flex-row">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/lst/app/imgs/dkLst.png"
|
||||
alt="Home"
|
||||
className="size-8 cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<div className="flex flex-row">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/lst/app/imgs/dkLst.png"
|
||||
alt="Home"
|
||||
className="size-8 cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
className="h-8 w-8"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none p-2">
|
||||
<span className="font-semibold text-2xl">Logistics Support Tool</span>
|
||||
</div>
|
||||
<div className="m-1 flex gap-1">
|
||||
<div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div>
|
||||
{session ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>
|
||||
Hello {session.user?.name}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link to="/">Profile</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* <DropdownMenuItem>Billing</DropdownMenuItem>
|
||||
<DropdownMenuItem>Team</DropdownMenuItem>
|
||||
<DropdownMenuItem>Subscription</DropdownMenuItem> */}
|
||||
<hr className="solid"></hr>
|
||||
<DropdownMenuItem>
|
||||
<div className="m-auto">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
navigate({ to: "/" });
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<div className="">
|
||||
<Button>
|
||||
<Link to="/login" search={{ redirect: currentPath }}>
|
||||
Login
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="h-8 w-8"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none w-52">
|
||||
<span className="font-semibold">Logistics Support Tool</span>
|
||||
</div>
|
||||
<div className="m-1">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
110
frontend/src/components/ui/avatar.tsx
Normal file
110
frontend/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn(
|
||||
"aspect-square size-full rounded-full object-cover",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarBadge,
|
||||
}
|
||||
103
frontend/src/components/ui/card.tsx
Normal file
103
frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", 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-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
31
frontend/src/components/ui/checkbox.tsx
Normal file
31
frontend/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as React from "react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
22
frontend/src/components/ui/label.tsx
Normal file
22
frontend/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
33
frontend/src/lib/formSutff/CheckBox.Field.tsx
Normal file
33
frontend/src/lib/formSutff/CheckBox.Field.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useFieldContext } from ".";
|
||||
import { FieldErrors } from "./Errors.Field";
|
||||
|
||||
type CheckboxFieldProps = {
|
||||
label: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const CheckboxField = ({ label }: CheckboxFieldProps) => {
|
||||
const field = useFieldContext<boolean>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor={field.name}>
|
||||
<span>{label}</span>
|
||||
</Label>
|
||||
|
||||
<Checkbox
|
||||
id={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.handleChange(checked === true);
|
||||
}}
|
||||
onBlur={field.handleBlur}
|
||||
/>
|
||||
</div>
|
||||
<FieldErrors meta={field.state.meta} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
frontend/src/lib/formSutff/Errors.Field.tsx
Normal file
18
frontend/src/lib/formSutff/Errors.Field.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AnyFieldMeta } from "@tanstack/react-form";
|
||||
|
||||
type FieldErrorsProps = {
|
||||
meta: AnyFieldMeta;
|
||||
};
|
||||
|
||||
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
|
||||
if (!meta.isTouched) return null;
|
||||
|
||||
return meta.errors.map((error) => (
|
||||
<p
|
||||
key={`${error.message}-${error.code ?? "err"}`}
|
||||
className="text-sm font-medium text-destructive"
|
||||
>
|
||||
{error.message}
|
||||
</p>
|
||||
));
|
||||
};
|
||||
37
frontend/src/lib/formSutff/Input.Field.tsx
Normal file
37
frontend/src/lib/formSutff/Input.Field.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useFieldContext } from ".";
|
||||
import { FieldErrors } from "./Errors.Field";
|
||||
|
||||
type InputFieldProps = {
|
||||
label: string;
|
||||
inputType: string;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
const autoCompleteMap: Record<string, string> = {
|
||||
email: "email",
|
||||
password: "current-password",
|
||||
text: "off",
|
||||
username: "username",
|
||||
};
|
||||
|
||||
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
|
||||
const field = useFieldContext<any>();
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={field.name}>{label}</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
autoComplete={autoCompleteMap[inputType] ?? "off"}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
type={inputType}
|
||||
required={required}
|
||||
/>
|
||||
<FieldErrors meta={field.state.meta} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
49
frontend/src/lib/formSutff/InputPassword.Field.tsx
Normal file
49
frontend/src/lib/formSutff/InputPassword.Field.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useFieldContext } from ".";
|
||||
import { FieldErrors } from "./Errors.Field";
|
||||
|
||||
type PasswordInputProps = {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
export const InputPasswordField = ({
|
||||
label,
|
||||
required = false,
|
||||
}: PasswordInputProps) => {
|
||||
const field = useFieldContext<any>();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="grid gap-3 mt-2">
|
||||
<Label htmlFor={field.name}>{label}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={field.name}
|
||||
type={show ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
required={required}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShow((p) => !p)}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<FieldErrors meta={field.state.meta} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
frontend/src/lib/formSutff/SubmitButton.tsx
Normal file
24
frontend/src/lib/formSutff/SubmitButton.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useFormContext } from ".";
|
||||
|
||||
type SubmitButtonProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SubmitButton = ({ children }: SubmitButtonProps) => {
|
||||
const form = useFormContext();
|
||||
|
||||
const [isSubmitting] = useStore(form.store, (state) => [
|
||||
state.isSubmitting,
|
||||
state.canSubmit,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{children}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
frontend/src/lib/formSutff/index.tsx
Normal file
23
frontend/src/lib/formSutff/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||
import { CheckboxField } from "./CheckBox.Field";
|
||||
import { InputField } from "./Input.Field";
|
||||
import { InputPasswordField } from "./InputPassword.Field";
|
||||
import { SubmitButton } from "./SubmitButton";
|
||||
|
||||
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
||||
createFormHookContexts();
|
||||
|
||||
export const { useAppForm } = createFormHook({
|
||||
fieldComponents: {
|
||||
InputField,
|
||||
InputPasswordField,
|
||||
//SelectField,
|
||||
CheckboxField,
|
||||
//DateField,
|
||||
//TextArea,
|
||||
//Searchable,
|
||||
},
|
||||
formComponents: { SubmitButton },
|
||||
fieldContext,
|
||||
formContext,
|
||||
});
|
||||
@@ -13,7 +13,7 @@ const queryClient = new QueryClient({
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5,
|
||||
retry: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnWindowFocus: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,9 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
|
||||
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
||||
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
@@ -28,35 +31,81 @@ const AdminLogsRoute = AdminLogsRouteImport.update({
|
||||
path: '/admin/logs',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const authLoginRoute = authLoginRouteImport.update({
|
||||
id: '/(auth)/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const authUserSignupRoute = authUserSignupRouteImport.update({
|
||||
id: '/(auth)/user/signup',
|
||||
path: '/user/signup',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({
|
||||
id: '/(auth)/user/resetpassword',
|
||||
path: '/user/resetpassword',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
'/user/signup': typeof authUserSignupRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
'/user/signup': typeof authUserSignupRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/(auth)/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
'/(auth)/user/signup': typeof authUserSignupRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about' | '/admin/logs'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/login'
|
||||
| '/admin/logs'
|
||||
| '/user/resetpassword'
|
||||
| '/user/signup'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about' | '/admin/logs'
|
||||
id: '__root__' | '/' | '/about' | '/admin/logs'
|
||||
to:
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/login'
|
||||
| '/admin/logs'
|
||||
| '/user/resetpassword'
|
||||
| '/user/signup'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/(auth)/login'
|
||||
| '/admin/logs'
|
||||
| '/(auth)/user/resetpassword'
|
||||
| '/(auth)/user/signup'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
authLoginRoute: typeof authLoginRoute
|
||||
AdminLogsRoute: typeof AdminLogsRoute
|
||||
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||
authUserSignupRoute: typeof authUserSignupRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -82,13 +131,37 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AdminLogsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(auth)/login': {
|
||||
id: '/(auth)/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof authLoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(auth)/user/signup': {
|
||||
id: '/(auth)/user/signup'
|
||||
path: '/user/signup'
|
||||
fullPath: '/user/signup'
|
||||
preLoaderRoute: typeof authUserSignupRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(auth)/user/resetpassword': {
|
||||
id: '/(auth)/user/resetpassword'
|
||||
path: '/user/resetpassword'
|
||||
fullPath: '/user/resetpassword'
|
||||
preLoaderRoute: typeof authUserResetpasswordRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
authLoginRoute: authLoginRoute,
|
||||
AdminLogsRoute: AdminLogsRoute,
|
||||
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||
authUserSignupRoute: authUserSignupRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
113
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal file
113
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useAppForm } from "@/lib/formSutff";
|
||||
|
||||
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
||||
const loginEmail = localStorage.getItem("loginEmail") || "";
|
||||
const rememberMe = localStorage.getItem("rememberMe") === "true";
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
email: loginEmail,
|
||||
password: "",
|
||||
rememberMe: rememberMe,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
// set remember me incase we want it later
|
||||
if (value.rememberMe) {
|
||||
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
||||
localStorage.setItem("loginEmail", value.email);
|
||||
} else {
|
||||
localStorage.removeItem("rememberMe");
|
||||
localStorage.removeItem("loginEmail");
|
||||
}
|
||||
|
||||
try {
|
||||
const login = await authClient.signIn.email({
|
||||
email: value.email,
|
||||
password: value.password,
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
navigate({ to: redirectPath ?? "/" });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (login.error) {
|
||||
toast.error(`${login.error?.message}`);
|
||||
return;
|
||||
}
|
||||
toast.success(`Welcome back ${login.data?.user.name}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="p-3 w-96">
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your username and password below
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form.AppField name="email">
|
||||
{(field) => (
|
||||
<field.InputField label="Email" inputType="email" required />
|
||||
)}
|
||||
</form.AppField>
|
||||
<form.AppField name="password">
|
||||
{(field) => (
|
||||
<field.InputPasswordField label="Password" required={true} />
|
||||
)}
|
||||
</form.AppField>
|
||||
|
||||
<div className="flex flex-row justify-between mt-3 mb-3">
|
||||
<form.AppField name="rememberMe">
|
||||
{(field) => <field.CheckboxField label="Remember me" />}
|
||||
</form.AppField>
|
||||
|
||||
<Link
|
||||
to="/user/resetpassword"
|
||||
className="inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<form.AppForm>
|
||||
<form.SubmitButton>Login</form.SubmitButton>
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-center mt-2">
|
||||
Don't have an account?{" "}
|
||||
<Link to={"/user/signup"} className="underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
frontend/src/routes/(auth)/login.tsx
Normal file
32
frontend/src/routes/(auth)/login.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import z from "zod";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import LoginForm from "./-components/LoginForm";
|
||||
|
||||
export const Route = createFileRoute("/(auth)/login")({
|
||||
component: RouteComponent,
|
||||
validateSearch: z.object({
|
||||
redirect: z.string().optional(),
|
||||
}),
|
||||
|
||||
beforeLoad: async () => {
|
||||
const result = await authClient.getSession({
|
||||
query: { disableCookieCache: true },
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
throw redirect({ to: "/" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const search = Route.useSearch();
|
||||
const redirectPath = search.redirect ?? "/";
|
||||
|
||||
return (
|
||||
<div className="flex justify-center mt-10">
|
||||
<LoginForm redirectPath={redirectPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
frontend/src/routes/(auth)/user.resetpassword.tsx
Normal file
9
frontend/src/routes/(auth)/user.resetpassword.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(auth)/user/resetpassword')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/(auth)/user/resetpassword"!</div>
|
||||
}
|
||||
9
frontend/src/routes/(auth)/user.signup.tsx
Normal file
9
frontend/src/routes/(auth)/user.signup.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(auth)/user/signup')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/(auth)/user/signup"!</div>
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { Toaster } from "sonner";
|
||||
import Header from "@/components/Header";
|
||||
import { AppSidebar } from "@/components/Sidebar/sidebar";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ThemeProvider } from "@/lib/theme-provider";
|
||||
|
||||
const RootLayout = () => (
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import z from "zod";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { authClient, useSession } from "../lib/auth-client";
|
||||
|
||||
import { useSession } from "../lib/auth-client";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
validateSearch: z.object({
|
||||
@@ -18,44 +14,13 @@ export const Route = createFileRoute("/")({
|
||||
|
||||
function Index() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const search = Route.useSearch();
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
await authClient.signIn.email({
|
||||
email: "blake.matthes@alpla.com",
|
||||
password: "nova0511",
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
navigate({ to: search.redirect ?? "/" });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("logged in");
|
||||
} catch (error: any) {
|
||||
console.error(error.response);
|
||||
}
|
||||
|
||||
//console.log(session)
|
||||
router.invalidate();
|
||||
};
|
||||
|
||||
if (isPending) return <div>Loading...</div>;
|
||||
if (isPending)
|
||||
return <div className="flex justify-center mt-10">Loading...</div>;
|
||||
// if (!session) return <button>Sign In</button>
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="flex justify-center mt-10">
|
||||
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
|
||||
{!session ? (
|
||||
<Button onClick={login}>Sign In</Button>
|
||||
) : (
|
||||
<div>
|
||||
<span>welcome {session.user?.name}</span>
|
||||
<Button onClick={() => authClient.signOut()}>Signout</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user