Skip to content

Commit

Permalink
Merge pull request #19 from ucmo-cs/feat/login
Browse files Browse the repository at this point in the history
Feat: full login functionality with frontend based auth
  • Loading branch information
michaelharlow authored Oct 31, 2024
2 parents df15efb + a431859 commit 1c7479f
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 22 deletions.
38 changes: 35 additions & 3 deletions src/main/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/main/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
Expand All @@ -18,9 +19,11 @@
"lucide-react": "^0.453.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.1",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
Expand Down
36 changes: 36 additions & 0 deletions src/main/frontend/src/components/Auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {createContext, useContext, useState} from "react";

const AuthContext = createContext<AuthContextType | undefined>(undefined);

function AuthProvider({ children}: Props) {
const [user, setUser] = useState<Login | undefined>(() => {
const user = sessionStorage.getItem("user");
return user ? JSON.parse(user) : undefined;
});

const signIn = (data: Login) => {
setUser(data);
sessionStorage.setItem("user", JSON.stringify(data));
};

const signOut = () => {
setUser(undefined);
sessionStorage.removeItem("user");
};

return (
<AuthContext.Provider value={{user, signIn, signOut}}>
{children}
</AuthContext.Provider>
);
}

const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

export { AuthProvider, useAuth };
13 changes: 13 additions & 0 deletions src/main/frontend/src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "@/components/Auth.tsx";

function ProtectedRoute() {
const {user} = useAuth();
console.log(`User2: ${user}`);
if (user === undefined) {
return <Navigate to={"/login"} replace />;
}
return <Outlet />;
}

export default ProtectedRoute;
2 changes: 1 addition & 1 deletion src/main/frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
Expand Down
176 changes: 176 additions & 0 deletions src/main/frontend/src/components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"

import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"

const Form = FormProvider

type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)

const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}

const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()

const fieldState = getFieldState(fieldContext.name, formState)

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}

const { id } = itemContext

return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}

type FormItemContextValue = {
id: string
}

const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)

const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()

return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"

const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()

return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"

const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()

return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"

const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()

return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"

const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children

if (!body) {
return null
}

return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"

export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
14 changes: 11 additions & 3 deletions src/main/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import {createBrowserRouter, createRoutesFromElements, Route, RouterProvider} from "react-router-dom";
import {createBrowserRouter, createRoutesFromElements, Navigate, Route, RouterProvider} from "react-router-dom";
import Layout from "@/components/Layout.tsx";
import Login from "@/routes/Login.tsx";
import { AuthProvider } from "@/components/Auth.tsx";
import ProtectedRoute from "@/components/ProtectedRoute.tsx";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path={"/"} element={<Layout />}>
<Route index element={<App />} /> {/* element={<Navigate to={"login"} replace />} */}
<Route index element={<Navigate to={"login"} replace />} />
<Route path={"login"} element={<Login />} />
<Route path={"register"} element={<h1>HELLO OTHER ROUTE</h1>} />
<Route path={"/"} element={<ProtectedRoute />}>
<Route path={"app-demo"} element={<App />} />
</Route>
</Route>
)
)

createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</StrictMode>,
)
Loading

0 comments on commit 1c7479f

Please sign in to comment.