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,
+}
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
new file mode 100644
index 0000000..9bd5a25
--- /dev/null
+++ b/frontend/src/components/ui/card.tsx
@@ -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 (
+
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 (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..cec7a77
--- /dev/null
+++ b/frontend/src/components/ui/checkbox.tsx
@@ -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
) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
new file mode 100644
index 0000000..f752f82
--- /dev/null
+++ b/frontend/src/components/ui/label.tsx
@@ -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) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/frontend/src/lib/formSutff/CheckBox.Field.tsx b/frontend/src/lib/formSutff/CheckBox.Field.tsx
new file mode 100644
index 0000000..6742548
--- /dev/null
+++ b/frontend/src/lib/formSutff/CheckBox.Field.tsx
@@ -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();
+
+ return (
+
+
+
+
+ {
+ field.handleChange(checked === true);
+ }}
+ onBlur={field.handleBlur}
+ />
+
+
+
+ );
+};
diff --git a/frontend/src/lib/formSutff/Errors.Field.tsx b/frontend/src/lib/formSutff/Errors.Field.tsx
new file mode 100644
index 0000000..86f1ec2
--- /dev/null
+++ b/frontend/src/lib/formSutff/Errors.Field.tsx
@@ -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) => (
+
+ {error.message}
+
+ ));
+};
diff --git a/frontend/src/lib/formSutff/Input.Field.tsx b/frontend/src/lib/formSutff/Input.Field.tsx
new file mode 100644
index 0000000..bdbb203
--- /dev/null
+++ b/frontend/src/lib/formSutff/Input.Field.tsx
@@ -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 = {
+ email: "email",
+ password: "current-password",
+ text: "off",
+ username: "username",
+};
+
+export const InputField = ({ label, inputType, required }: InputFieldProps) => {
+ const field = useFieldContext();
+
+ return (
+
+
+ field.handleChange(e.target.value)}
+ onBlur={field.handleBlur}
+ type={inputType}
+ required={required}
+ />
+
+
+ );
+};
diff --git a/frontend/src/lib/formSutff/InputPassword.Field.tsx b/frontend/src/lib/formSutff/InputPassword.Field.tsx
new file mode 100644
index 0000000..cee3633
--- /dev/null
+++ b/frontend/src/lib/formSutff/InputPassword.Field.tsx
@@ -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();
+ const [show, setShow] = useState(false);
+
+ return (
+
+
+
+ field.handleChange(e.target.value)}
+ onBlur={field.handleBlur}
+ required={required}
+ className="pr-10"
+ />
+
+
+
+
+ );
+};
diff --git a/frontend/src/lib/formSutff/SubmitButton.tsx b/frontend/src/lib/formSutff/SubmitButton.tsx
new file mode 100644
index 0000000..f3ecfdc
--- /dev/null
+++ b/frontend/src/lib/formSutff/SubmitButton.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/frontend/src/lib/formSutff/index.tsx b/frontend/src/lib/formSutff/index.tsx
new file mode 100644
index 0000000..b784505
--- /dev/null
+++ b/frontend/src/lib/formSutff/index.tsx
@@ -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,
+});
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 6044abb..d781095 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -13,7 +13,7 @@ const queryClient = new QueryClient({
queries: {
staleTime: 1000 * 60 * 5,
retry: 0,
- refetchOnWindowFocus: false,
+ refetchOnWindowFocus: true,
},
},
});
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 99b9fc3..b721a60 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -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)
diff --git a/frontend/src/routes/(auth)/-components/LoginForm.tsx b/frontend/src/routes/(auth)/-components/LoginForm.tsx
new file mode 100644
index 0000000..fed4b9d
--- /dev/null
+++ b/frontend/src/routes/(auth)/-components/LoginForm.tsx
@@ -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 (
+
+
+
+ Login to your account
+
+ Enter your username and password below
+
+
+
+
+ {(field) => (
+
+ )}
+
+
+ {(field) => (
+
+ )}
+
+
+
+
+ {(field) => }
+
+
+
+ Forgot your password?
+
+
+
+
+
+ Login
+
+
+
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/(auth)/login.tsx b/frontend/src/routes/(auth)/login.tsx
new file mode 100644
index 0000000..888e9df
--- /dev/null
+++ b/frontend/src/routes/(auth)/login.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/(auth)/user.resetpassword.tsx b/frontend/src/routes/(auth)/user.resetpassword.tsx
new file mode 100644
index 0000000..14b6a40
--- /dev/null
+++ b/frontend/src/routes/(auth)/user.resetpassword.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(auth)/user/resetpassword')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/(auth)/user/resetpassword"!
+}
diff --git a/frontend/src/routes/(auth)/user.signup.tsx b/frontend/src/routes/(auth)/user.signup.tsx
new file mode 100644
index 0000000..faeca71
--- /dev/null
+++ b/frontend/src/routes/(auth)/user.signup.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(auth)/user/signup')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/(auth)/user/signup"!
+}
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx
index 8f61db1..4a0b74a 100644
--- a/frontend/src/routes/__root.tsx
+++ b/frontend/src/routes/__root.tsx
@@ -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 = () => (
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index d7702aa..6e95d2b 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -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 Loading...
;
+ if (isPending)
+ return Loading...
;
// if (!session) return
return (
-
+
Welcome Home!
- {!session ? (
-
- ) : (
-
- welcome {session.user?.name}
-
-
- )}
);
}