login form created
This commit is contained in:
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Logistics Support Tool</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 { SidebarIcon } from "lucide-react";
|
||||||
|
import { authClient, useSession } from "@/lib/auth-client";
|
||||||
import { ModeToggle } from "./mode-toggle";
|
import { ModeToggle } from "./mode-toggle";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "./ui/dropdown-menu";
|
||||||
import { useSidebar } from "./ui/sidebar";
|
import { useSidebar } from "./ui/sidebar";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { toggleSidebar } = useSidebar();
|
const { toggleSidebar } = useSidebar();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const { signOut } = authClient;
|
||||||
|
const router = useRouterState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentPath = router.location.href;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 flex w-full items-center border-b bg-background">
|
<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 justify-between w-full">
|
||||||
<div className="flex flex-row">
|
<div className="flex items-center gap-2 px-4">
|
||||||
<Link to="/">
|
<div className="flex flex-row">
|
||||||
<img
|
<Link to="/">
|
||||||
src="/lst/app/imgs/dkLst.png"
|
<img
|
||||||
alt="Home"
|
src="/lst/app/imgs/dkLst.png"
|
||||||
className="size-8 cursor-pointer"
|
alt="Home"
|
||||||
/>
|
className="size-8 cursor-pointer"
|
||||||
</Link>
|
/>
|
||||||
|
</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>
|
</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>
|
</div>
|
||||||
</header>
|
</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: {
|
queries: {
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
retry: 0,
|
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 AboutRouteImport } from './routes/about'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
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({
|
const AboutRoute = AboutRouteImport.update({
|
||||||
id: '/about',
|
id: '/about',
|
||||||
@@ -28,35 +31,81 @@ const AdminLogsRoute = AdminLogsRouteImport.update({
|
|||||||
path: '/admin/logs',
|
path: '/admin/logs',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
|
'/(auth)/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/about' | '/admin/logs'
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/login'
|
||||||
|
| '/admin/logs'
|
||||||
|
| '/user/resetpassword'
|
||||||
|
| '/user/signup'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/about' | '/admin/logs'
|
to:
|
||||||
id: '__root__' | '/' | '/about' | '/admin/logs'
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/login'
|
||||||
|
| '/admin/logs'
|
||||||
|
| '/user/resetpassword'
|
||||||
|
| '/user/signup'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/(auth)/login'
|
||||||
|
| '/admin/logs'
|
||||||
|
| '/(auth)/user/resetpassword'
|
||||||
|
| '/(auth)/user/signup'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AboutRoute: typeof AboutRoute
|
AboutRoute: typeof AboutRoute
|
||||||
|
authLoginRoute: typeof authLoginRoute
|
||||||
AdminLogsRoute: typeof AdminLogsRoute
|
AdminLogsRoute: typeof AdminLogsRoute
|
||||||
|
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||||
|
authUserSignupRoute: typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -82,13 +131,37 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AdminLogsRouteImport
|
preLoaderRoute: typeof AdminLogsRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
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 = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AboutRoute: AboutRoute,
|
AboutRoute: AboutRoute,
|
||||||
|
authLoginRoute: authLoginRoute,
|
||||||
AdminLogsRoute: AdminLogsRoute,
|
AdminLogsRoute: AdminLogsRoute,
|
||||||
|
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||||
|
authUserSignupRoute: authUserSignupRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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 { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import { AppSidebar } from "@/components/Sidebar/sidebar";
|
import { AppSidebar } from "@/components/Sidebar/sidebar";
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
|
||||||
import { ThemeProvider } from "@/lib/theme-provider";
|
import { ThemeProvider } from "@/lib/theme-provider";
|
||||||
|
|
||||||
const RootLayout = () => (
|
const RootLayout = () => (
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import {
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
createFileRoute,
|
|
||||||
useNavigate,
|
|
||||||
useRouter,
|
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
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("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: z.object({
|
validateSearch: z.object({
|
||||||
@@ -18,44 +14,13 @@ export const Route = createFileRoute("/")({
|
|||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { data: session, isPending } = useSession();
|
const { data: session, isPending } = useSession();
|
||||||
const router = useRouter();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const search = Route.useSearch();
|
|
||||||
|
|
||||||
const login = async () => {
|
if (isPending)
|
||||||
try {
|
return <div className="flex justify-center mt-10">Loading...</div>;
|
||||||
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 (!session) return <button>Sign In</button>
|
// if (!session) return <button>Sign In</button>
|
||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className="flex justify-center mt-10">
|
||||||
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user