From 047ffbf5355b5b9318eb0e4b3afeccb19987a35c Mon Sep 17 00:00:00 2001 From: Sam Piper Date: Sat, 13 Apr 2024 18:09:41 +0100 Subject: [PATCH] Changes: feat: - added new components for routing specific states including unfinished and generic error - useCurrentApp is a new hook to work out which "app" you are on based on the route tree - removed the need for a redux reducer - added unfinished component for routes that are unfinished fix: - authentication.controller.ts handles conflicts chore: - refactored how types are handled in forge - refactored the redux reducers and redux folder --- .../authentication.controller.ts | 8 +- apps/forge/src/api/axiosInstance.ts | 10 +- .../src/components/auth-provider/index.tsx | 47 +- .../src/components/navbar/appNav/appLinks.ts | 88 ++-- .../src/components/navbar/appNav/index.tsx | 52 +-- .../components/navbar/appSwitcher/index.tsx | 29 +- apps/forge/src/components/navbar/index.tsx | 33 +- .../src/components/routing/GenericError.tsx | 37 ++ .../forge/src/components/routing/NotFound.tsx | 38 ++ .../components/routing/RouteUnfinished.tsx | 38 ++ .../actions/ActiveLocationSelector/index.tsx | 278 ++++++------ .../signin/actions/QueueDispatcher/index.tsx | 260 ++++++----- .../actions/RegisterDispatcher/index.tsx | 248 +++++----- .../signin/actions/SignInDispatcher/index.tsx | 2 +- .../signin/actions/SignInManager/index.tsx | 422 +++++++++--------- .../actions/SignInReasonInput/index.tsx | 2 +- .../actions/SignInRegisterForm/index.tsx | 137 +++--- .../actions/SignOutDispatcher/index.tsx | 245 +++++----- .../actions/ToolSelectionInput/index.tsx | 2 +- .../signin/actions/UCardInput/index.tsx | 136 +++--- apps/forge/src/config/constants.ts | 8 +- apps/forge/src/config/nav.ts | 14 + apps/forge/src/hooks/useCurrentApp.ts | 28 ++ apps/forge/src/hooks/useLogout.ts | 35 ++ .../src/hooks/useVerifyAuthentication.ts | 38 ++ .../src/hooks/useVerifyAuthentication.tsx | 38 -- apps/forge/src/redux/app/app.slice.ts | 25 -- apps/forge/src/redux/app/app.types.ts | 6 - apps/forge/src/redux/{auth => }/auth.slice.ts | 13 +- apps/forge/src/redux/common/common.types.ts | 1 - .../src/redux/{signin => }/signin.slice.ts | 17 +- .../forge/src/redux/signin/signin.defaults.ts | 9 - apps/forge/src/redux/store.ts | 90 ++-- apps/forge/src/redux/{user => }/user.slice.ts | 6 +- apps/forge/src/routes/__root.tsx | 91 ++-- .../routes/_authenticated/printing/index.tsx | 26 +- .../_authenticated/user/profile/index.tsx | 3 +- .../_authenticated/user/settings/index.tsx | 25 +- apps/forge/src/routes/auth/login/complete.tsx | 43 +- apps/forge/src/routes/auth/login/index.tsx | 73 ++- apps/forge/src/routes/auth/logout/index.tsx | 48 +- apps/forge/src/services/auth/loginService.ts | 22 - apps/forge/src/services/auth/logoutService.ts | 32 -- apps/forge/src/types/app.ts | 1 + .../auth/auth.types.ts => types/auth.ts} | 0 apps/forge/src/types/common.ts | 6 + .../signin.types.ts => types/signin.ts} | 0 .../user/user.types.ts => types/user.ts} | 2 +- 48 files changed, 1442 insertions(+), 1370 deletions(-) create mode 100644 apps/forge/src/components/routing/GenericError.tsx create mode 100644 apps/forge/src/components/routing/NotFound.tsx create mode 100644 apps/forge/src/components/routing/RouteUnfinished.tsx create mode 100644 apps/forge/src/config/nav.ts create mode 100644 apps/forge/src/hooks/useCurrentApp.ts create mode 100644 apps/forge/src/hooks/useLogout.ts create mode 100644 apps/forge/src/hooks/useVerifyAuthentication.ts delete mode 100644 apps/forge/src/hooks/useVerifyAuthentication.tsx delete mode 100644 apps/forge/src/redux/app/app.slice.ts delete mode 100644 apps/forge/src/redux/app/app.types.ts rename apps/forge/src/redux/{auth => }/auth.slice.ts (79%) delete mode 100644 apps/forge/src/redux/common/common.types.ts rename apps/forge/src/redux/{signin => }/signin.slice.ts (79%) delete mode 100644 apps/forge/src/redux/signin/signin.defaults.ts rename apps/forge/src/redux/{user => }/user.slice.ts (78%) delete mode 100644 apps/forge/src/services/auth/loginService.ts delete mode 100644 apps/forge/src/services/auth/logoutService.ts create mode 100644 apps/forge/src/types/app.ts rename apps/forge/src/{redux/auth/auth.types.ts => types/auth.ts} (100%) create mode 100644 apps/forge/src/types/common.ts rename apps/forge/src/{redux/signin/signin.types.ts => types/signin.ts} (100%) rename apps/forge/src/{redux/user/user.types.ts => types/user.ts} (62%) diff --git a/apps/anvil/src/auth/authentication/authentication.controller.ts b/apps/anvil/src/auth/authentication/authentication.controller.ts index 2c945c3..0e05d8f 100644 --- a/apps/anvil/src/auth/authentication/authentication.controller.ts +++ b/apps/anvil/src/auth/authentication/authentication.controller.ts @@ -5,6 +5,7 @@ import type { User } from "@ignis/types/users"; import { BadRequestException, Body, + ConflictException, Controller, Get, Post, @@ -19,6 +20,7 @@ import { Throttle } from "@nestjs/throttler"; import { Request, Response } from "express"; import { AuthenticationService } from "./authentication.service"; import { BlacklistService } from "./blacklist/blacklist.service"; +import { CardinalityViolationError } from "edgedb"; @Controller("authentication") export class AuthenticationController { @@ -80,7 +82,11 @@ export class AuthenticationController { // Add the token to the blacklist const expiryDate = new Date(); - await this.blacklistService.addToBlacklist(refreshToken, expiryDate); + try { + await this.blacklistService.addToBlacklist(refreshToken, expiryDate); + } catch (error) { + throw new ConflictException("Refresh token is invalid or expired"); + } this.authService.clearAuthCookies(res); diff --git a/apps/forge/src/api/axiosInstance.ts b/apps/forge/src/api/axiosInstance.ts index 923eb03..f660f6d 100644 --- a/apps/forge/src/api/axiosInstance.ts +++ b/apps/forge/src/api/axiosInstance.ts @@ -1,6 +1,4 @@ import axios from "axios"; -import { useDispatch } from "react-redux"; -import useLogout from "@/services/auth/useLogout.ts"; const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_URL, @@ -14,16 +12,14 @@ const RETRY_DELAY = 1000; // Starting retry delay in milliseconds axiosInstance.interceptors.response.use( (response) => response, async (error) => { - const dispatch = useDispatch(); - const logout = useLogout(dispatch); const originalRequest = error.config; if (error.response && error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; originalRequest._retryCount = originalRequest._retryCount || 0; if (originalRequest._retryCount >= RETRY_LIMIT) { - await logout; return Promise.reject("Retry limit reached. Logging out..."); + //TODO WORK OUT HOW TO LOGOUT / WORK THIS BIT OUT IM NOT SURE EXACTLY WHAT IM DOING HERE RN } originalRequest._retryCount++; @@ -34,11 +30,11 @@ axiosInstance.interceptors.response.use( await axiosInstance.post("/authentication/refresh"); return axiosInstance(originalRequest); } catch (refreshError) { - await logout; return Promise.reject(refreshError); } + } else { + return Promise.reject(error); } - return Promise.reject(error); }, ); diff --git a/apps/forge/src/components/auth-provider/index.tsx b/apps/forge/src/components/auth-provider/index.tsx index f937ae7..4dcfe5e 100644 --- a/apps/forge/src/components/auth-provider/index.tsx +++ b/apps/forge/src/components/auth-provider/index.tsx @@ -1,43 +1,38 @@ import * as React from "react"; -import {User} from "@ignis/types/users.ts"; -import {useVerifyAuthentication} from "@/hooks/useVerifyAuthentication.tsx"; -import {Loader} from "@ui/components/ui/loader.tsx"; +import { User } from "@ignis/types/users.ts"; +import { useVerifyAuthentication } from "@/hooks/useVerifyAuthentication.ts"; +import { Loader } from "@ui/components/ui/loader.tsx"; export interface AuthContext { - isAuthenticated: boolean; - user: User | null; - logout: () => void; + isAuthenticated: boolean; + user: User | null; + logout: () => void; } const AuthContext = React.createContext(null); export function AuthProvider({ children }: { children: React.ReactNode }) { - const { user, loading, setUser } = useVerifyAuthentication(); + const { user, loading, setUser } = useVerifyAuthentication(); - if (loading) { - return ; - } + if (loading) { + return ; + } - const isAuthenticated = !!user; + const isAuthenticated = !!user; + const logout = () => { + setUser(null); + }; - const logout = () => { - setUser(null) - } + const contextValue = { isAuthenticated, user, logout }; - const contextValue = { isAuthenticated, user, logout }; - - return ( - - {children} - - ); + return {children}; } export function useAuth() { - const context = React.useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; + const context = React.useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; } diff --git a/apps/forge/src/components/navbar/appNav/appLinks.ts b/apps/forge/src/components/navbar/appNav/appLinks.ts index 6f5070d..ae6b3be 100644 --- a/apps/forge/src/components/navbar/appNav/appLinks.ts +++ b/apps/forge/src/components/navbar/appNav/appLinks.ts @@ -1,50 +1,56 @@ // appLinks.ts -import {Apps} from "@/redux/app/app.types.ts"; +import { Apps } from "@/types/app.ts"; export type AppLink = { - app: Apps; // Identifier for the app - displayName: string; // User-facing name - path?: string; // Path for the link - children?: AppLink[]; // Nested links, specific to the same app - index?: number; // Index of the link in the navbar (PER LEVEL) - id: string; // Unique identifier for the link + app: Apps; // Identifier for the app + displayName: string; // User-facing name + path?: string; // Path for the link + children?: AppLink[]; // Nested links, specific to the same app + index?: number; // Index of the link in the navbar (PER LEVEL) + id: string; // Unique identifier for the link }; export const appLinks: AppLink[] = [ - {app: "Main", displayName: "Home", path: "/", index: 0, id: "home"}, - {app: "Sign In", displayName: "Sign In Home", path: "/signin", index: 0, id: "signin_root"}, - { + { app: "Main", displayName: "Home", path: "/", index: 0, id: "home" }, + { app: "Sign In", displayName: "Sign In Home", path: "/signin", index: 0, id: "signin_root" }, + { + app: "Sign In", + displayName: "Agreements", + path: "/signin/agreements", + index: 1, + id: "signin_agreements", + }, + { + app: "Sign In", + displayName: "Actions", + path: "/signin/actions", + index: 2, + id: "signin_actions_root", + children: [ + { app: "Sign In", - displayName: "Agreements", - path: "/signin/agreements", - index: 1, - id: "signin_agreements" - }, - { + displayName: "Sign In", + path: "/signin/actions/in", + index: 0, + id: "signin_actions_in", + }, + { app: "Sign In", displayName: "Sign Out", path: "/signin/actions/out", id: "signin_actions_out", index: 1 }, + { app: "Sign In", - displayName: "Actions", - path: "/signin/actions", + displayName: "Register", + path: "/signin/actions/register", + id: "signin_actions_register", index: 2, - id: "signin_actions_root", - children: [ - { - app: "Sign In", - displayName: "Sign In", - path: "/signin/actions/in", - index: 0, - id: "signin_actions_in", - }, - {app: "Sign In", displayName: "Sign Out", path: "/signin/actions/out", id: "signin_actions_out", index: 1}, - {app: "Sign In", displayName: "Register", path: "/signin/actions/register", id: "signin_actions_register", index: 2}, - { - app: "Sign In", - displayName: "Enqueue", - path: "/signin/actions/enqueue", - id: "signin_actions_enqueue", - index: 3, - }, - ] - }, - {app: "Sign In", displayName: "Dashboard", path: "/signin/dashboard", index: 1, id: "signin_status"}, - {app: "Printing", displayName: "Printing", path: "/printing", id: "printing_root", index: 0}, -]; \ No newline at end of file + }, + { + app: "Sign In", + displayName: "Enqueue", + path: "/signin/actions/enqueue", + id: "signin_actions_enqueue", + index: 3, + }, + ], + }, + { app: "Sign In", displayName: "Dashboard", path: "/signin/dashboard", index: 1, id: "signin_status" }, + { app: "Printing", displayName: "Printing", path: "/printing", id: "printing_root", index: 0 }, +]; diff --git a/apps/forge/src/components/navbar/appNav/index.tsx b/apps/forge/src/components/navbar/appNav/index.tsx index c2bd6f2..f214fc3 100644 --- a/apps/forge/src/components/navbar/appNav/index.tsx +++ b/apps/forge/src/components/navbar/appNav/index.tsx @@ -1,29 +1,31 @@ // NavBar.tsx -import {Link} from "@tanstack/react-router"; -import {useSelector} from "react-redux"; -import {AppRootState} from "@/redux/store"; -import {appLinks} from "./appLinks"; -import {AppLinkDropdown} from './appLinkDropdown.tsx'; +import { Link } from "@tanstack/react-router"; +import { appLinks } from "./appLinks"; +import { AppLinkDropdown } from "./appLinkDropdown.tsx"; -export default function AppNav() { - const currentApp = useSelector((state: AppRootState) => state.app.current_app); - const sortedAppLinks = appLinks.sort((a, b) => (a.index ?? 0) - (b.index ?? 0)); +import useCurrentApp from "@/hooks/useCurrentApp.ts"; - return ( -
- {sortedAppLinks.map((link) => { - if (link.app === currentApp) { - return link.children && link.children.length > 0 ? ( - - ) : ( - - {link.displayName} - - ); - } - return null; - })} -
- ); +export default function AppNav() { + const currentApp = useCurrentApp(); + const sortedAppLinks = appLinks.sort((a, b) => (a.index ?? 0) - (b.index ?? 0)); + return ( +
+ {sortedAppLinks.map((link) => { + if (link.app === currentApp) { + return link.children && link.children.length > 0 ? ( + + ) : ( + + {link.displayName} + + ); + } + return null; + })} +
+ ); } diff --git a/apps/forge/src/components/navbar/appSwitcher/index.tsx b/apps/forge/src/components/navbar/appSwitcher/index.tsx index b5a1c4d..b77a6e1 100644 --- a/apps/forge/src/components/navbar/appSwitcher/index.tsx +++ b/apps/forge/src/components/navbar/appSwitcher/index.tsx @@ -1,8 +1,5 @@ import ContextMenuWrapper from "@/components/navbar/appSwitcher/ContextMenu.tsx"; import ListItem from "@/components/navbar/appSwitcher/ListItem"; -import { appActions } from "@/redux/app/app.slice"; -import { Apps } from "@/redux/app/app.types"; -import { AppRootState } from "@/redux/store"; import { Link } from "@tanstack/react-router"; import { NavigationMenu, @@ -13,23 +10,17 @@ import { NavigationMenuTrigger, } from "@ui/components/ui/navigation-menu"; import { PenLine, Printer, Share2 } from "lucide-react"; -import { useDispatch, useSelector } from "react-redux"; +import useCurrentApp from "@/hooks/useCurrentApp.ts"; export default function AppSwitcher() { - const dispatch = useDispatch(); - const app = useSelector((state: AppRootState) => state.app.current_app); - - // Function to handle app change - const handleAppChange = (newApp: Apps) => { - dispatch(appActions.setApp(newApp)); - }; + const currentapp = useCurrentApp(); return ( - iForge | {app} + iForge | {currentapp}
    @@ -38,7 +29,6 @@ export default function AppSwitcher() { handleAppChange("Main")} >
    iForge

    @@ -47,19 +37,14 @@ export default function AppSwitcher() { - } onClick={() => handleAppChange("Sign In")}> + }> Remote queue and space access for easy sign-in. - } onClick={() => handleAppChange("Training")}> + }> Handle your iForge training here. - } - onClick={() => handleAppChange("Printing")} - > - Advanced 3D printing to realize your creative designs. + }> + 3D Printing WIP

diff --git a/apps/forge/src/components/navbar/index.tsx b/apps/forge/src/components/navbar/index.tsx index 4d9c53f..7cb6a3b 100644 --- a/apps/forge/src/components/navbar/index.tsx +++ b/apps/forge/src/components/navbar/index.tsx @@ -1,22 +1,23 @@ import AppSwitcher from "@/components/navbar/appSwitcher"; -import {ThemeSwitcher} from "@/components/navbar/themeSwitcher"; -import {UserNav} from "@/components/navbar/userNav"; +import { ThemeSwitcher } from "@/components/navbar/themeSwitcher"; +import { UserNav } from "@/components/navbar/userNav"; import AppNav from "@/components/navbar/appNav"; export default function NavBar() { - return ( -
-
- -
-
-
-
- - -
-
+ return ( +
+
+ +
+
+ +
+
+
+ +
- ); +
+
+ ); } diff --git a/apps/forge/src/components/routing/GenericError.tsx b/apps/forge/src/components/routing/GenericError.tsx new file mode 100644 index 0000000..2debeff --- /dev/null +++ b/apps/forge/src/components/routing/GenericError.tsx @@ -0,0 +1,37 @@ +import { useNavigate } from "@tanstack/react-router"; +import Title from "@/components/title"; +import { Button } from "@ui/components/ui/button.tsx"; +import React from "react"; + +export const GenericError = () => { + const navigate = useNavigate(); + const goToHome = () => navigate({ to: "/" }); + + return ( + + + <div className="flex items-center justify-center w-full min-h-[80vh] px-4"> + <div className="grid items-center gap-4 text-center"> + <div className="space-y-2"> + <h1 className="text-4xl font-bold tracking-tighter sm:text-6xl">Oopsie! - It broke.</h1> + <p className="max-w-[600px] mx-auto text-accent-foreground md:text-xl/relaxed"> + Oops! Something has broken! You can try to close to tab and navigate to the site again! + </p> + <p className="text-sm text-accent-foreground"> + Note: This site is still under development. For assistance, join our{" "} + <a className="text-primary" href={import.meta.env.VITE_DISCORD_URL}> + Discord server + </a> + . + </p> + </div> + <div className="flex justify-center"> + <Button variant="outline" onClick={goToHome}> + Or go to Homepage + </Button> + </div> + </div> + </div> + </React.Fragment> + ); +}; diff --git a/apps/forge/src/components/routing/NotFound.tsx b/apps/forge/src/components/routing/NotFound.tsx new file mode 100644 index 0000000..bda0c93 --- /dev/null +++ b/apps/forge/src/components/routing/NotFound.tsx @@ -0,0 +1,38 @@ +import { useNavigate } from "@tanstack/react-router"; +import Title from "@/components/title"; +import { Button } from "@ui/components/ui/button.tsx"; +import React from "react"; + +export const NotFound = () => { + const navigate = useNavigate(); + + const goToHome = () => navigate({ to: "/" }); + + return ( + <React.Fragment> + <Title prompt="Not Found" /> + <div className="flex items-center justify-center w-full min-h-[80vh] px-4"> + <div className="grid items-center gap-4 text-center"> + <div className="space-y-2"> + <h1 className="text-4xl font-bold tracking-tighter sm:text-6xl">404 - Page Not Found</h1> + <p className="max-w-[600px] mx-auto text-accent-foreground md:text-xl/relaxed"> + Oops! The page you are looking for does not exist or is under construction. + </p> + <p className="text-sm text-accent-foreground"> + Note: This site is still under development. For assistance, join our{" "} + <a className="text-primary" href={import.meta.env.VITE_DISCORD_URL}> + Discord server + </a> + . + </p> + </div> + <div className="flex justify-center"> + <Button variant="outline" onClick={goToHome}> + Go to Homepage + </Button> + </div> + </div> + </div> + </React.Fragment> + ); +}; diff --git a/apps/forge/src/components/routing/RouteUnfinished.tsx b/apps/forge/src/components/routing/RouteUnfinished.tsx new file mode 100644 index 0000000..46b44b8 --- /dev/null +++ b/apps/forge/src/components/routing/RouteUnfinished.tsx @@ -0,0 +1,38 @@ +import { useNavigate } from "@tanstack/react-router"; +import Title from "@/components/title"; +import { Button } from "@ui/components/ui/button.tsx"; +import React from "react"; + +export const RouteUnfinished = () => { + const navigate = useNavigate(); + + const goToHome = () => navigate({ to: "/" }); + + return ( + <React.Fragment> + <Title prompt="Unfinished" /> + <div className="flex items-center justify-center w-full min-h-[80vh] px-4"> + <div className="grid items-center gap-4 text-center"> + <div className="space-y-2"> + <h1 className="text-4xl font-bold tracking-tighter sm:text-6xl">Under Construction</h1> + <p className="max-w-[600px] mx-auto text-accent-foreground md:text-xl/relaxed"> + This page is currently being worked on! And will be finished soon! + </p> + <p className="text-sm text-accent-foreground"> + Note: This site is still under development. For assistance, join our{" "} + <a className="text-primary" href={import.meta.env.VITE_DISCORD_URL}> + Discord server + </a> + . + </p> + </div> + <div className="flex justify-center"> + <Button variant="outline" onClick={goToHome}> + Go to Homepage + </Button> + </div> + </div> + </div> + </React.Fragment> + ); +}; diff --git a/apps/forge/src/components/signin/actions/ActiveLocationSelector/index.tsx b/apps/forge/src/components/signin/actions/ActiveLocationSelector/index.tsx index f717582..ce21cc6 100644 --- a/apps/forge/src/components/signin/actions/ActiveLocationSelector/index.tsx +++ b/apps/forge/src/components/signin/actions/ActiveLocationSelector/index.tsx @@ -1,139 +1,153 @@ // @ts-ignore -import {CaretSortIcon, CheckIcon} from "@radix-ui/react-icons" -import {cn} from "@/lib/utils" -import {Button} from "@ui/components/ui/button" -import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem,} from "@ui/components/ui/command" -import {Popover, PopoverContent, PopoverTrigger,} from "@ui/components/ui/popover" -import {useLayoutEffect, useState} from "react"; -import {useDispatch, useSelector} from "react-redux"; -import {AppDispatch, AppRootState} from "@/redux/store.ts"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {locationStatus} from "@/services/signin/locationService.ts"; -import {useQuery} from "@tanstack/react-query"; -import {Separator} from "@ui/components/ui/separator.tsx"; -import {PulseLoader} from "react-spinners"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { cn } from "@/lib/utils"; +import { Button } from "@ui/components/ui/button"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@ui/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/ui/popover"; +import { useLayoutEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { locationStatus } from "@/services/signin/locationService.ts"; +import { useQuery } from "@tanstack/react-query"; +import { Separator } from "@ui/components/ui/separator.tsx"; +import { PulseLoader } from "react-spinners"; const ActiveLocationSelector = () => { - const [open, setOpen] = useState<boolean>(false); - const [value, setValue] = useState<string>(""); - const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); - const refetchInterval = 5000; - - const {data: locationStatuses, isLoading, isError, error} = useQuery({ - queryKey: ['locationStatus'], - queryFn: locationStatus, - refetchInterval: value ? refetchInterval : false, // Only refetch if a location is selected - }); - - - const borderColor = !locationStatuses || isError ? 'border-red-500' : 'border-transparent'; - - const dispatch: AppDispatch = useDispatch(); - - useLayoutEffect(() => { - if (locationStatuses) { - dispatch(signinActions.setLocations(locationStatuses)); - dispatch(signinActions.clearError()); - } - if (isError) { - dispatch(signinActions.setError(error.message)); - } - - - if (activeLocation != "") { - setValue(activeLocation) - } - - }, [locationStatuses, dispatch, isError, activeLocation, error]); - - - const capitalizeFirstLetter = (string: string) => { - return string.charAt(0).toUpperCase() + string.slice(1); - }; - - const handleLocationSelect = (selectedLocationName: string) => { - dispatch(signinActions.setActiveLocation(selectedLocationName)); - }; - - - const activeLocationStatus = locationStatuses?.find(status => status.locationName === activeLocation); - - return ( - <> - <div - className="flex items-center justify-between p-3 space-x-4 bg-navbar text-navbar-foreground mt-4 mb-4 drop-shadow-lg dark:shadow-none flex-col md:flex-row"> - <div> - <span className="text-gray-700 dark:text-white font-medium mr-2">Select Location</span> - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <Button - variant="outline" - role="combobox" - aria-expanded={open} - className={`w-[200px] justify-between border-2 ${borderColor}`} - > - {value ? capitalizeFirstLetter(value) : "No active location selected"} - <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50"/> - </Button> - </PopoverTrigger> - <PopoverContent className="w-[200px] p-0"> - <Command> - <CommandInput placeholder="Search locations..." className="h-9"/> - {(!isLoading && <CommandEmpty>No locations found.</CommandEmpty>)} - <CommandGroup> - {(isLoading ? <div className="flex items-center justify-center h-[40px]"><PulseLoader color="#e11d48" size={15}/> </div>: <> - {locationStatuses && locationStatuses!.map((location, index) => ( - <CommandItem - key={index} - value={location.locationName} - onSelect={(currentValue) => { - setValue(currentValue); - setOpen(false); - handleLocationSelect(currentValue); - }} - > - {capitalizeFirstLetter(location.locationName)} - <CheckIcon - className={cn( - "ml-auto h-4 w-4", - value === location.locationName ? "opacity-100" : "opacity-0" - )} - /> - </CommandItem> - ))} </>)} - </CommandGroup> - </Command> - </PopoverContent> - </Popover> - </div> - {isLoading && <PulseLoader color="#e11d48" size={10}/>} - {activeLocationStatus && !isLoading && ( - <div className="flex items-center gap-2 mt-2 lg:mt-0"> - <span className="text-gray-500 dark:text-gray-400">Status</span> - {/* Open Status */} - {(activeLocationStatus.open ? (<span className="text-green-500">OPEN</span>) : ( - <span className="text-red-500">CLOSED</span>))} - <Separator orientation="vertical"/> - {/* Count and Max Count Status */} - - <span className="text-gray-500 dark:text-gray-400">Current Users </span> - <span - className="text-navbar-foreground"> {activeLocationStatus.count}/{activeLocationStatus.max}</span> - <span className="text-gray-500 dark:text-gray-400">Max Users </span> - {/* Queue Status */} - - {(activeLocationStatus.needs_queue ? (<span className="text-red-500">Queue Needed</span>) : ( - <span className="text-green-500">No Queue Needed</span>))} - <Separator orientation="vertical"/> - <span className="text-navbar-foreground"> {activeLocationStatus.count_in_queue}</span> - <span className="text-gray-500 dark:text-gray-400">in Queue</span> + const [open, setOpen] = useState<boolean>(false); + const [value, setValue] = useState<string>(""); + const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); + const refetchInterval = 5000; + + const { + data: locationStatuses, + isLoading, + isError, + error, + } = useQuery({ + queryKey: ["locationStatus"], + queryFn: locationStatus, + refetchInterval: value ? refetchInterval : false, // Only refetch if a location is selected + }); + + const borderColor = !locationStatuses || isError ? "border-red-500" : "border-transparent"; + + const dispatch: AppDispatch = useDispatch(); + + useLayoutEffect(() => { + if (locationStatuses) { + dispatch(signinActions.setLocations(locationStatuses)); + dispatch(signinActions.clearError()); + } + if (isError) { + dispatch(signinActions.setError(error.message)); + } + + if (activeLocation != "") { + setValue(activeLocation); + } + }, [locationStatuses, dispatch, isError, activeLocation, error]); + + const capitalizeFirstLetter = (string: string) => { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + const handleLocationSelect = (selectedLocationName: string) => { + dispatch(signinActions.setActiveLocation(selectedLocationName)); + }; + + const activeLocationStatus = locationStatuses?.find((status) => status.locationName === activeLocation); + + return ( + <> + <div className="flex items-center justify-between p-3 space-x-4 bg-navbar text-navbar-foreground mt-4 mb-4 drop-shadow-lg dark:shadow-none flex-col md:flex-row"> + <div> + <span className="text-gray-700 dark:text-white font-medium mr-2">Select Location</span> + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className={`w-[200px] justify-between border-2 ${borderColor}`} + > + {value ? capitalizeFirstLetter(value) : "No active location selected"} + <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[200px] p-0"> + <Command> + <CommandInput placeholder="Search locations..." className="h-9" /> + {!isLoading && <CommandEmpty>No locations found.</CommandEmpty>} + <CommandGroup> + {isLoading ? ( + <div className="flex items-center justify-center h-[40px]"> + <PulseLoader color="#e11d48" size={15} />{" "} </div> - )} - </div> - </> - ); + ) : ( + <> + {locationStatuses && + locationStatuses!.map((location, index) => ( + <CommandItem + key={index} + value={location.locationName} + onSelect={(currentValue) => { + setValue(currentValue); + setOpen(false); + handleLocationSelect(currentValue); + }} + > + {capitalizeFirstLetter(location.locationName)} + <CheckIcon + className={cn( + "ml-auto h-4 w-4", + value === location.locationName ? "opacity-100" : "opacity-0", + )} + /> + </CommandItem> + ))}{" "} + </> + )} + </CommandGroup> + </Command> + </PopoverContent> + </Popover> + </div> + {isLoading && <PulseLoader color="#e11d48" size={10} />} + {activeLocationStatus && !isLoading && ( + <div className="flex items-center gap-2 mt-2 lg:mt-0"> + <span className="text-gray-500 dark:text-gray-400">Status</span> + {/* Open Status */} + {activeLocationStatus.open ? ( + <span className="text-green-500">OPEN</span> + ) : ( + <span className="text-red-500">CLOSED</span> + )} + <Separator orientation="vertical" /> + {/* Count and Max Count Status */} + + <span className="text-gray-500 dark:text-gray-400">Current Users </span> + <span className="text-navbar-foreground"> + {" "} + {activeLocationStatus.count}/{activeLocationStatus.max} + </span> + <span className="text-gray-500 dark:text-gray-400">Max Users </span> + {/* Queue Status */} + + {activeLocationStatus.needs_queue ? ( + <span className="text-red-500">Queue Needed</span> + ) : ( + <span className="text-green-500">No Queue Needed</span> + )} + <Separator orientation="vertical" /> + <span className="text-navbar-foreground"> {activeLocationStatus.count_in_queue}</span> + <span className="text-gray-500 dark:text-gray-400">in Queue</span> + </div> + )} + </div> + </> + ); }; -export default ActiveLocationSelector - +export default ActiveLocationSelector; diff --git a/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx b/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx index 90ab4b3..caff0b5 100644 --- a/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/QueueDispatcher/index.tsx @@ -1,138 +1,132 @@ -import {Alert, AlertDescription, AlertTitle} from "@ui/components/ui/alert.tsx"; -import {ExclamationTriangleIcon} from "@radix-ui/react-icons"; -import {useMutation} from "@tanstack/react-query"; -import {AppDispatch, AppRootState} from "@/redux/store.ts"; -import {useDispatch, useSelector} from "react-redux"; -import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; - -import {Loader} from "@ui/components/ui/loader.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {useState} from "react"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {FlowStepComponent} from "@/components/signin/actions/SignInManager/types.ts"; -import {useNavigate} from "@tanstack/react-router"; -import {toast} from "sonner"; -import {PostQueueInPerson, PostQueueProps} from "@/services/signin/queueService.ts"; - - -const QueueDispatcher: FlowStepComponent = ({onSecondary, onPrimary}) => { - const dispatch: AppDispatch = useDispatch(); - const signInSession = useSelector((state: AppRootState) => state.signin.session) - const activeLocation = useSelector((state: AppRootState) => state.signin.active_location) - const abortController = new AbortController(); // For gracefully cancelling the query - const [canContinue, setCanContinue] = useState<boolean>(false) - const navigate = useNavigate() - const timeout = 3000 - - const queueProps: PostQueueProps = { - locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, - signal: abortController.signal, - }; - - - const {isPending, error, mutate} = useMutation({ - mutationKey: ['postQueueInPerson', queueProps], - mutationFn: () => PostQueueInPerson(queueProps), - retry: 0, - onError: (error) => { - console.log('Error', error) - abortController.abort(); - }, - onSuccess: () => { - console.log('Success') - setCanContinue(true) - abortController.abort(); - redirectToActions(timeout) - toast.success('User added to queue successfully') - }, - }); - - const errorDisplay = (error: Error | null) => { - - if (error) { - - return ( - <Alert variant="destructive"> - <ExclamationTriangleIcon className="h-4 w-4"/> - <AlertTitle>Error</AlertTitle> - <AlertDescription> - {error.message} - </AlertDescription> - </Alert> - ) - - } - return ( - - <> - <Alert variant="destructive"> - <ExclamationTriangleIcon className="h-4 w-4"/> - <AlertTitle>Error</AlertTitle> - <AlertDescription> - There was an error with your session, try again! <br/> - Error: "Unknown" - </AlertDescription> - </Alert> - </> - ) +import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert.tsx"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { useDispatch, useSelector } from "react-redux"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; + +import { Loader } from "@ui/components/ui/loader.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { useState } from "react"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { PostQueueInPerson, PostQueueProps } from "@/services/signin/queueService.ts"; + +const QueueDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { + const dispatch: AppDispatch = useDispatch(); + const signInSession = useSelector((state: AppRootState) => state.signin.session); + const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); + const abortController = new AbortController(); // For gracefully cancelling the query + const [canContinue, setCanContinue] = useState<boolean>(false); + const navigate = useNavigate(); + const timeout = 3000; + + const queueProps: PostQueueProps = { + locationName: activeLocation, + uCardNumber: signInSession?.ucard_number ?? 0, + signal: abortController.signal, + }; + + const { isPending, error, mutate } = useMutation({ + mutationKey: ["postQueueInPerson", queueProps], + mutationFn: () => PostQueueInPerson(queueProps), + retry: 0, + onError: (error) => { + console.log("Error", error); + abortController.abort(); + }, + onSuccess: () => { + console.log("Success"); + setCanContinue(true); + abortController.abort(); + redirectToActions(timeout); + toast.success("User added to queue successfully"); + }, + }); + + const errorDisplay = (error: Error | null) => { + if (error) { + return ( + <Alert variant="destructive"> + <ExclamationTriangleIcon className="h-4 w-4" /> + <AlertTitle>Error</AlertTitle> + <AlertDescription>{error.message}</AlertDescription> + </Alert> + ); } - - const redirectToActions = (timeoutInMs: number) => { - setTimeout(() => { - dispatch(signinActions.resetSignInSession()); - navigate({to: '/signin/actions'}) - }, timeoutInMs) - } - - - const successDisplay = ( - <> - <div className="flex justify-items-center justify-center"> - <h1 className="text-xl flex-auto">Success!</h1> - <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> - </div> - </> - ) - - const handleSecondaryClick = () => { - abortController.abort(); - onSecondary?.(); - } - - const handlePrimaryClick = () => { - if (canContinue) { - abortController.abort(); - onPrimary?.(); - console.log('Done ') - dispatch(signinActions.resetSignInSession()); - } - } - - return ( - <> - <Card className="w-[700px]"> - <CardHeader> - <CardTitle>Adding User to Queue</CardTitle> - </CardHeader> - <CardContent> - {!canContinue && !error && !isPending && ( - <Button onClick={() => mutate()} autoFocus={true} variant="outline" - className="h-[200px] w-full">Join Queue</Button> - )} - {isPending && <Loader/>} - {!isPending && error && !canContinue && errorDisplay(error)} - {!isPending && canContinue && successDisplay} - </CardContent> - <CardFooter className="flex justify-between flex-row-reverse"> - <Button onClick={handlePrimaryClick} disabled={!canContinue}>Continue</Button> - <Button onClick={handleSecondaryClick} variant="outline">Go Back</Button> - </CardFooter> - </Card> - </> + <> + <Alert variant="destructive"> + <ExclamationTriangleIcon className="h-4 w-4" /> + <AlertTitle>Error</AlertTitle> + <AlertDescription> + There was an error with your session, try again! <br /> + Error: "Unknown" + </AlertDescription> + </Alert> + </> ); + }; + + const redirectToActions = (timeoutInMs: number) => { + setTimeout(() => { + dispatch(signinActions.resetSignInSession()); + navigate({ to: "/signin/actions" }); + }, timeoutInMs); + }; + + const successDisplay = ( + <> + <div className="flex justify-items-center justify-center"> + <h1 className="text-xl flex-auto">Success!</h1> + <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> + </div> + </> + ); + + const handleSecondaryClick = () => { + abortController.abort(); + onSecondary?.(); + }; + + const handlePrimaryClick = () => { + if (canContinue) { + abortController.abort(); + onPrimary?.(); + console.log("Done "); + dispatch(signinActions.resetSignInSession()); + } + }; + + return ( + <> + <Card className="w-[700px]"> + <CardHeader> + <CardTitle>Adding User to Queue</CardTitle> + </CardHeader> + <CardContent> + {!canContinue && !error && !isPending && ( + <Button onClick={() => mutate()} autoFocus={true} variant="outline" className="h-[200px] w-full"> + Join Queue + </Button> + )} + {isPending && <Loader />} + {!isPending && error && !canContinue && errorDisplay(error)} + {!isPending && canContinue && successDisplay} + </CardContent> + <CardFooter className="flex justify-between flex-row-reverse"> + <Button onClick={handlePrimaryClick} disabled={!canContinue}> + Continue + </Button> + <Button onClick={handleSecondaryClick} variant="outline"> + Go Back + </Button> + </CardFooter> + </Card> + </> + ); }; - -export default QueueDispatcher \ No newline at end of file +export default QueueDispatcher; diff --git a/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx b/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx index 8744714..8bbbd4e 100644 --- a/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/RegisterDispatcher/index.tsx @@ -1,128 +1,128 @@ -import {Alert, AlertDescription, AlertTitle} from "@ui/components/ui/alert.tsx"; -import {ExclamationTriangleIcon} from "@radix-ui/react-icons"; -import {PostRegister, PostRegisterProps} from "@/services/signin/signInService.ts"; -import {useMutation} from "@tanstack/react-query"; -import {AppDispatch, AppRootState} from "@/redux/store.ts"; -import {useDispatch, useSelector} from "react-redux"; -import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; - -import {Loader} from "@ui/components/ui/loader.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {useState} from "react"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {FlowStepComponent} from "@/components/signin/actions/SignInManager/types.ts"; -import {useNavigate} from "@tanstack/react-router"; -import {toast} from "sonner"; - - -const RegisterDispatcher: FlowStepComponent = ({onSecondary, onPrimary}) => { - const dispatch: AppDispatch = useDispatch(); - const signInSession = useSelector((state: AppRootState) => state.signin.session) - const activeLocation = useSelector((state: AppRootState) => state.signin.active_location) - const abortController = new AbortController(); // For gracefully cancelling the query - const [canContinue, setCanContinue] = useState<boolean>(false) - const navigate = useNavigate() - const timeout = 3000 - - const registerProps: PostRegisterProps = { - locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, - signal: abortController.signal, - username: signInSession?.username ?? "" - }; - - - const {isPending, error, mutate} = useMutation({ - mutationKey: ['postRegister', registerProps], - mutationFn: () => PostRegister(registerProps), - retry: 0, - onMutate: () => { - setTimeout(() => { - console.log('Aborting request deadline exceeded') - abortController.abort(); - } , 5000) - }, - onError: (error) => { - console.log('Error', error) - abortController.abort(); - }, - onSuccess: () => { - console.log('Success') - setCanContinue(true) - abortController.abort(); - redirectToActions(timeout) - toast.success('User registered successfully') - }, - }); - - const errorDisplay = (error: Error | null) => ( - <> - <Alert variant="destructive"> - <ExclamationTriangleIcon className="h-4 w-4"/> - <AlertTitle>Error</AlertTitle> - <AlertDescription> - There was an error with your session, try again! <br/> - Error: {error?.message ?? "Unknown"} - </AlertDescription> - </Alert> - </> - ) - - const redirectToActions = (timeoutInMs: number) => { - setTimeout(() => { - dispatch(signinActions.resetSignInSession()); - navigate({to: '/signin/actions'}) - }, timeoutInMs) - } - - - const successDisplay = ( - <> - <div className="flex justify-items-center justify-center"> - <h1 className="text-xl flex-auto">Success!</h1> - <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> - </div> - </> - ) - - const handleSecondaryClick = () => { +import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert.tsx"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { PostRegister, PostRegisterProps } from "@/services/signin/signInService.ts"; +import { useMutation } from "@tanstack/react-query"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { useDispatch, useSelector } from "react-redux"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; + +import { Loader } from "@ui/components/ui/loader.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { useState } from "react"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; + +const RegisterDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { + const dispatch: AppDispatch = useDispatch(); + const signInSession = useSelector((state: AppRootState) => state.signin.session); + const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); + const abortController = new AbortController(); // For gracefully cancelling the query + const [canContinue, setCanContinue] = useState<boolean>(false); + const navigate = useNavigate(); + const timeout = 3000; + + const registerProps: PostRegisterProps = { + locationName: activeLocation, + uCardNumber: signInSession?.ucard_number ?? 0, + signal: abortController.signal, + username: signInSession?.username ?? "", + }; + + const { isPending, error, mutate } = useMutation({ + mutationKey: ["postRegister", registerProps], + mutationFn: () => PostRegister(registerProps), + retry: 0, + onMutate: () => { + setTimeout(() => { + console.log("Aborting request deadline exceeded"); abortController.abort(); - onSecondary?.(); - } - - const handlePrimaryClick = () => { - if (canContinue) { - abortController.abort(); - onPrimary?.(); - console.log('Done ') - dispatch(signinActions.resetSignInSession()); - } + }, 5000); + }, + onError: (error) => { + console.log("Error", error); + abortController.abort(); + }, + onSuccess: () => { + console.log("Success"); + setCanContinue(true); + abortController.abort(); + redirectToActions(timeout); + toast.success("User registered successfully"); + }, + }); + + const errorDisplay = (error: Error | null) => ( + <> + <Alert variant="destructive"> + <ExclamationTriangleIcon className="h-4 w-4" /> + <AlertTitle>Error</AlertTitle> + <AlertDescription> + There was an error with your session, try again! <br /> + Error: {error?.message ?? "Unknown"} + </AlertDescription> + </Alert> + </> + ); + + const redirectToActions = (timeoutInMs: number) => { + setTimeout(() => { + dispatch(signinActions.resetSignInSession()); + navigate({ to: "/signin/actions" }); + }, timeoutInMs); + }; + + const successDisplay = ( + <> + <div className="flex justify-items-center justify-center"> + <h1 className="text-xl flex-auto">Success!</h1> + <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> + </div> + </> + ); + + const handleSecondaryClick = () => { + abortController.abort(); + onSecondary?.(); + }; + + const handlePrimaryClick = () => { + if (canContinue) { + abortController.abort(); + onPrimary?.(); + console.log("Done "); + dispatch(signinActions.resetSignInSession()); } - - - return ( - <> - <Card className="w-[700px]"> - <CardHeader> - <CardTitle>Registering User</CardTitle> - </CardHeader> - <CardContent> - {!canContinue && !error && !isPending && ( - <Button onClick={() => mutate()} autoFocus={true} variant="outline" - className="h-[200px] w-full">Register</Button> - )} - {isPending && <Loader/>} - {!isPending && error && !canContinue && errorDisplay(error)} - {!isPending && canContinue && successDisplay} - </CardContent> - <CardFooter className="flex justify-between flex-row-reverse"> - <Button onClick={handlePrimaryClick} disabled={!canContinue}>Continue</Button> - <Button onClick={handleSecondaryClick} variant="outline">Go Back</Button> - </CardFooter> - </Card> - </> - ); + }; + + return ( + <> + <Card className="w-[700px]"> + <CardHeader> + <CardTitle>Registering User</CardTitle> + </CardHeader> + <CardContent> + {!canContinue && !error && !isPending && ( + <Button onClick={() => mutate()} autoFocus={true} variant="outline" className="h-[200px] w-full"> + Register + </Button> + )} + {isPending && <Loader />} + {!isPending && error && !canContinue && errorDisplay(error)} + {!isPending && canContinue && successDisplay} + </CardContent> + <CardFooter className="flex justify-between flex-row-reverse"> + <Button onClick={handlePrimaryClick} disabled={!canContinue}> + Continue + </Button> + <Button onClick={handleSecondaryClick} variant="outline"> + Go Back + </Button> + </CardFooter> + </Card> + </> + ); }; - -export default RegisterDispatcher \ No newline at end of file +export default RegisterDispatcher; diff --git a/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx b/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx index dd3d8b5..5335e98 100644 --- a/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInDispatcher/index.tsx @@ -9,7 +9,7 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/compon import { Loader } from "@ui/components/ui/loader.tsx"; import { Button } from "@ui/components/ui/button.tsx"; import { useEffect, useState } from "react"; -import { signinActions } from "@/redux/signin/signin.slice.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; import { useNavigate } from "@tanstack/react-router"; diff --git a/apps/forge/src/components/signin/actions/SignInManager/index.tsx b/apps/forge/src/components/signin/actions/SignInManager/index.tsx index 7c238fc..34ef808 100644 --- a/apps/forge/src/components/signin/actions/SignInManager/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInManager/index.tsx @@ -1,243 +1,239 @@ -import React, {ReactElement, useEffect, useLayoutEffect, useState} from "react"; +import React, { ReactElement, useEffect, useLayoutEffect, useState } from "react"; import UCardInput from "@/components/signin/actions/UCardInput"; import SignInReasonInput from "@/components/signin/actions/SignInReasonInput"; -import {useDispatch, useSelector} from "react-redux"; -import {AppDispatch, AppRootState} from "@/redux/store.ts"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {SignInSession} from "@/redux/signin/signin.types.ts"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { SignInSession } from "@/types/signin.ts"; import { - AnyStep, EnqueueSteps, FlowConfiguration, FlowStepComponent, - FlowType, - flowTypeToPrintTable, RegisterSteps, - SignInSteps, - SignOutSteps + AnyStep, + EnqueueSteps, + FlowConfiguration, + FlowStepComponent, + FlowType, + flowTypeToPrintTable, + RegisterSteps, + SignInSteps, + SignOutSteps, } from "@/components/signin/actions/SignInManager/types"; -import {Separator} from "@ui/components/ui/separator" +import { Separator } from "@ui/components/ui/separator"; import ToolSelectionInput from "@/components/signin/actions/ToolSelectionInput"; import SignInDispatcher from "@/components/signin/actions/SignInDispatcher"; import SignOutDispatcher from "@/components/signin/actions/SignOutDispatcher"; import SignInFlowProgress from "@/components/signin/actions/SignInFlowProgress"; -import {Button} from "@ui/components/ui/button.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; import useDoubleTapEscape from "@/components/signin/actions/SignInManager/useDoubleTapEscape.ts"; import QueueDispatcher from "@/components/signin/actions/QueueDispatcher"; import RegisterDispatcher from "@/components/signin/actions/RegisterDispatcher"; import SignInRegisterForm from "@/components/signin/actions/SignInRegisterForm"; const flowConfig: FlowConfiguration = { - [FlowType.SignIn]: { - [SignInSteps.Step1]: UCardInput, - [SignInSteps.Step2]: ToolSelectionInput, - [SignInSteps.Step3]: SignInReasonInput, - [SignInSteps.Step4]: SignInDispatcher, - }, - [FlowType.SignOut]: { - [SignOutSteps.Step1]: UCardInput, - [SignOutSteps.Step2]: SignOutDispatcher, - }, - [FlowType.Register]: { - [RegisterSteps.Step1]: UCardInput, - [RegisterSteps.Step2]: SignInRegisterForm, - [RegisterSteps.Step3]: RegisterDispatcher, - }, - [FlowType.Enqueue]: { - [EnqueueSteps.Step1]: UCardInput, - [EnqueueSteps.Step2]: QueueDispatcher, - } + [FlowType.SignIn]: { + [SignInSteps.Step1]: UCardInput, + [SignInSteps.Step2]: ToolSelectionInput, + [SignInSteps.Step3]: SignInReasonInput, + [SignInSteps.Step4]: SignInDispatcher, + }, + [FlowType.SignOut]: { + [SignOutSteps.Step1]: UCardInput, + [SignOutSteps.Step2]: SignOutDispatcher, + }, + [FlowType.Register]: { + [RegisterSteps.Step1]: UCardInput, + [RegisterSteps.Step2]: SignInRegisterForm, + [RegisterSteps.Step3]: RegisterDispatcher, + }, + [FlowType.Enqueue]: { + [EnqueueSteps.Step1]: UCardInput, + [EnqueueSteps.Step2]: QueueDispatcher, + }, }; const defaultSignInSession: SignInSession = { - ucard_number: 0, - is_rep: false, - sign_in_reason: null, - training: null, - navigation_is_backtracking: false, - session_errored: false, - username: null, -} + ucard_number: 0, + is_rep: false, + sign_in_reason: null, + training: null, + navigation_is_backtracking: false, + session_errored: false, + username: null, +}; interface SignInManagerProps { - initialFlow?: FlowType; + initialFlow?: FlowType; } export const getStepComponent = ( - currentFlow: FlowType, - currentStep: SignInSteps | SignOutSteps | RegisterSteps | EnqueueSteps, - flowConfig: FlowConfiguration + currentFlow: FlowType, + currentStep: SignInSteps | SignOutSteps | RegisterSteps | EnqueueSteps, + flowConfig: FlowConfiguration, ): FlowStepComponent => { - switch (currentFlow) { - case FlowType.SignIn: - return flowConfig[currentFlow][currentStep as SignInSteps]; - case FlowType.SignOut: - return flowConfig[currentFlow][currentStep as SignOutSteps]; - case FlowType.Register: - return flowConfig[currentFlow][currentStep as RegisterSteps]; - case FlowType.Enqueue: - return flowConfig[currentFlow][currentStep as EnqueueSteps]; - default: - throw new Error(`Unsupported flow type: ${currentFlow}`); - } + switch (currentFlow) { + case FlowType.SignIn: + return flowConfig[currentFlow][currentStep as SignInSteps]; + case FlowType.SignOut: + return flowConfig[currentFlow][currentStep as SignOutSteps]; + case FlowType.Register: + return flowConfig[currentFlow][currentStep as RegisterSteps]; + case FlowType.Enqueue: + return flowConfig[currentFlow][currentStep as EnqueueSteps]; + default: + throw new Error(`Unsupported flow type: ${currentFlow}`); + } }; // SignInActionsManager Component -const SignInActionsManager: React.FC<SignInManagerProps> = ({initialFlow}) => { - const [currentFlow, setCurrentFlow] = useState<FlowType | null>(null); - const [currentStep, setCurrentStep] = useState<AnyStep | null>(null); - const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); - - const dispatch = useDispatch<AppDispatch>(); - - const handleDoubleTapEscape = () => { - setCurrentFlow(null); - setCurrentStep(null); - }; - - useDoubleTapEscape(handleDoubleTapEscape); - - // Function to advance to the next step within the current flow - const moveToNextStep = () => { - if (currentFlow == null || currentStep == null) return; - - const steps = Object.keys(flowConfig[currentFlow]) as AnyStep[]; - const currentStepIndex = steps.indexOf(currentStep); - - if (currentStepIndex >= 0 && currentStepIndex < steps.length - 1) { - const nextStep = steps[currentStepIndex + 1]; - setCurrentStep(nextStep as AnyStep); - } else { - setCurrentFlow(null); - setCurrentStep(null); - } - }; - - // Function to go back to the previous step within the current flow - const moveToPreviousStep = () => { - if (currentFlow == null || currentStep == null) return; - - const steps = Object.keys(flowConfig[currentFlow]) as AnyStep[]; - const currentStepIndex = steps.indexOf(currentStep); - - if (currentStepIndex > 0) { - const previousStep = steps[currentStepIndex - 1]; - setCurrentStep(previousStep as AnyStep); - } else { - setCurrentFlow(null); - setCurrentStep(null); - } - }; - - const getStepIndex = (steps: AnyStep[], currentStep: AnyStep): number => { - return steps.indexOf(currentStep); - }; - - - // Make new Session - useEffect(() => { - dispatch(signinActions.setSignInSession(defaultSignInSession)) - }, []); - - useEffect(() => { - if (currentStepIndex > 1) { - handleDoubleTapEscape(); - } - }, [activeLocation]); - - useLayoutEffect(() => { - if (initialFlow) { - setCurrentFlow(initialFlow); - // Dynamically set the initial step for the initialFlow - const initialStep = Object.keys(flowConfig[initialFlow])[0] as AnyStep; - setCurrentStep(initialStep); - } - }, [initialFlow]); - - - // Function to initialize the flow - const startFlow = (flowType: FlowType) => { - setCurrentFlow(flowType); - // Dynamically set the initial step based on the flowType - const initialStep = Object.keys(flowConfig[flowType])[0] as AnyStep; - setCurrentStep(initialStep); - dispatch(signinActions.setSignInSession(defaultSignInSession)); - }; - - - const renderCurrentStep = (): ReactElement | null => { - if (currentFlow == null || currentStep == null) return null; - - - const StepComponent = getStepComponent(currentFlow, currentStep, flowConfig) - - if (StepComponent) { - // This is to stop the case where a rep is backtracking and then step 3 auto navigates them forwards again - if (currentStep == SignInSteps.Step3) { - dispatch(signinActions.updateSignInSessionField("navigation_is_backtracking", true)); - } - return StepComponent ? <StepComponent onPrimary={moveToNextStep} onSecondary={moveToPreviousStep}/> : null - } else { - // Handle the end of the flow or an invalid step - return <div>Flow completed or invalid step</div>; - } - }; - - const getTotalSteps = (flow: FlowType): number => { - return flow === FlowType.SignIn ? Object.keys(SignInSteps).length : Object.keys(SignOutSteps).length; - }; - - const totalSteps = currentFlow ? getTotalSteps(currentFlow) : 0; - const currentStepIndex = currentStep ? getStepIndex(Object.values(SignInSteps), currentStep) : 0; - - - return ( - <div className="border-2 p-4"> - <h1 className="text-xl font-bold mb-4 text-center">Sign In Actions</h1> - {currentFlow && (<div - className="flex items-center justify-between p-3 space-x-4 bg-navbar text-navbar-foreground mt-4 mb-4 drop-shadow-lg dark:shadow-none flex-col md:flex-row"> - <div className="flex items-center"> - <span className="text-lg font-bold mr-2">Current Flow:</span> - <span className="text-ring uppercase text-xl">{flowTypeToPrintTable(currentFlow)}</span> - </div> - <Button onClick={() => setCurrentFlow(null)}>Clear Flow</Button> - </div>)} - - <div className="flex flex-row"> - {currentFlow && ( - <> - <SignInFlowProgress currentStep={currentStep as AnyStep} flowType={currentFlow} - totalSteps={totalSteps}> - {/* Pass the current step's index and total steps */} - <div>{`Current Step: ${currentStepIndex + 1} of ${totalSteps}`}</div> - </SignInFlowProgress> - <Separator className='ml-2 mr-2' orientation="vertical"/> - <div className="ml-4">{renderCurrentStep()}</div> - </> - )} - - {!currentFlow && ( - <div className="flex flex-1 items-center justify-center"> - <div className="p-6 space-y-4 w-full max-w-2xl rounded-xl shadow-lg"> - <div className="grid grid-cols-2 gap-10"> - <Button variant="default" className="h-20" onClick={() => startFlow(FlowType.SignIn)}> - Start Sign In - </Button> - <Button variant="secondary" className="h-20" - onClick={() => startFlow(FlowType.SignOut)}> - Start Sign Out - </Button> - <Button variant="outline" className="h-20" - onClick={() => startFlow(FlowType.Register)}> - Start Register - </Button> - <Button variant="outline" className="h-20" - onClick={() => startFlow(FlowType.Enqueue)}> - Enqueue User - </Button> - </div> - </div> - </div> - )} - </div> +const SignInActionsManager: React.FC<SignInManagerProps> = ({ initialFlow }) => { + const [currentFlow, setCurrentFlow] = useState<FlowType | null>(null); + const [currentStep, setCurrentStep] = useState<AnyStep | null>(null); + const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); + + const dispatch = useDispatch<AppDispatch>(); + + const handleDoubleTapEscape = () => { + setCurrentFlow(null); + setCurrentStep(null); + }; + + useDoubleTapEscape(handleDoubleTapEscape); + + // Function to advance to the next step within the current flow + const moveToNextStep = () => { + if (currentFlow == null || currentStep == null) return; + + const steps = Object.keys(flowConfig[currentFlow]) as AnyStep[]; + const currentStepIndex = steps.indexOf(currentStep); + + if (currentStepIndex >= 0 && currentStepIndex < steps.length - 1) { + const nextStep = steps[currentStepIndex + 1]; + setCurrentStep(nextStep as AnyStep); + } else { + setCurrentFlow(null); + setCurrentStep(null); + } + }; + + // Function to go back to the previous step within the current flow + const moveToPreviousStep = () => { + if (currentFlow == null || currentStep == null) return; + + const steps = Object.keys(flowConfig[currentFlow]) as AnyStep[]; + const currentStepIndex = steps.indexOf(currentStep); + + if (currentStepIndex > 0) { + const previousStep = steps[currentStepIndex - 1]; + setCurrentStep(previousStep as AnyStep); + } else { + setCurrentFlow(null); + setCurrentStep(null); + } + }; + + const getStepIndex = (steps: AnyStep[], currentStep: AnyStep): number => { + return steps.indexOf(currentStep); + }; + + // Make new Session + useEffect(() => { + dispatch(signinActions.setSignInSession(defaultSignInSession)); + }, []); + + useEffect(() => { + if (currentStepIndex > 1) { + handleDoubleTapEscape(); + } + }, [activeLocation]); + + useLayoutEffect(() => { + if (initialFlow) { + setCurrentFlow(initialFlow); + // Dynamically set the initial step for the initialFlow + const initialStep = Object.keys(flowConfig[initialFlow])[0] as AnyStep; + setCurrentStep(initialStep); + } + }, [initialFlow]); + + // Function to initialize the flow + const startFlow = (flowType: FlowType) => { + setCurrentFlow(flowType); + // Dynamically set the initial step based on the flowType + const initialStep = Object.keys(flowConfig[flowType])[0] as AnyStep; + setCurrentStep(initialStep); + dispatch(signinActions.setSignInSession(defaultSignInSession)); + }; + + const renderCurrentStep = (): ReactElement | null => { + if (currentFlow == null || currentStep == null) return null; + + const StepComponent = getStepComponent(currentFlow, currentStep, flowConfig); + + if (StepComponent) { + // This is to stop the case where a rep is backtracking and then step 3 auto navigates them forwards again + if (currentStep == SignInSteps.Step3) { + dispatch(signinActions.updateSignInSessionField("navigation_is_backtracking", true)); + } + return StepComponent ? <StepComponent onPrimary={moveToNextStep} onSecondary={moveToPreviousStep} /> : null; + } else { + // Handle the end of the flow or an invalid step + return <div>Flow completed or invalid step</div>; + } + }; + + const getTotalSteps = (flow: FlowType): number => { + return flow === FlowType.SignIn ? Object.keys(SignInSteps).length : Object.keys(SignOutSteps).length; + }; + + const totalSteps = currentFlow ? getTotalSteps(currentFlow) : 0; + const currentStepIndex = currentStep ? getStepIndex(Object.values(SignInSteps), currentStep) : 0; + + return ( + <div className="border-2 p-4"> + <h1 className="text-xl font-bold mb-4 text-center">Sign In Actions</h1> + {currentFlow && ( + <div className="flex items-center justify-between p-3 space-x-4 bg-navbar text-navbar-foreground mt-4 mb-4 drop-shadow-lg dark:shadow-none flex-col md:flex-row"> + <div className="flex items-center"> + <span className="text-lg font-bold mr-2">Current Flow:</span> + <span className="text-ring uppercase text-xl">{flowTypeToPrintTable(currentFlow)}</span> + </div> + <Button onClick={() => setCurrentFlow(null)}>Clear Flow</Button> </div> - ); + )} + + <div className="flex flex-row"> + {currentFlow && ( + <> + <SignInFlowProgress currentStep={currentStep as AnyStep} flowType={currentFlow} totalSteps={totalSteps}> + {/* Pass the current step's index and total steps */} + <div>{`Current Step: ${currentStepIndex + 1} of ${totalSteps}`}</div> + </SignInFlowProgress> + <Separator className="ml-2 mr-2" orientation="vertical" /> + <div className="ml-4">{renderCurrentStep()}</div> + </> + )} + + {!currentFlow && ( + <div className="flex flex-1 items-center justify-center"> + <div className="p-6 space-y-4 w-full max-w-2xl rounded-xl shadow-lg"> + <div className="grid grid-cols-2 gap-10"> + <Button variant="default" className="h-20" onClick={() => startFlow(FlowType.SignIn)}> + Start Sign In + </Button> + <Button variant="secondary" className="h-20" onClick={() => startFlow(FlowType.SignOut)}> + Start Sign Out + </Button> + <Button variant="outline" className="h-20" onClick={() => startFlow(FlowType.Register)}> + Start Register + </Button> + <Button variant="outline" className="h-20" onClick={() => startFlow(FlowType.Enqueue)}> + Enqueue User + </Button> + </div> + </div> + </div> + )} + </div> + </div> + ); }; export default SignInActionsManager; diff --git a/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx b/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx index 06acbc4..afa446e 100644 --- a/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInReasonInput/index.tsx @@ -1,5 +1,5 @@ import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types"; -import { signinActions } from "@/redux/signin/signin.slice"; +import { signinActions } from "@/redux/signin.slice.ts"; import { AppDispatch, AppRootState } from "@/redux/store"; import { useSignInReasons } from "@/services/signin/signInReasonService"; import type { Reason, ReasonCategory } from "@ignis/types/sign_in"; diff --git a/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx b/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx index df462a0..3cf6be7 100644 --- a/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx +++ b/apps/forge/src/components/signin/actions/SignInRegisterForm/index.tsx @@ -1,78 +1,81 @@ -import {FlowStepComponent} from "@/components/signin/actions/SignInManager/types.ts"; -import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {Input} from "@ui/components/ui/input.tsx"; -import React, {useState} from "react"; -import {useDispatch} from "react-redux"; -import {AppDispatch} from "@/redux/store.ts"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; +import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { Input } from "@ui/components/ui/input.tsx"; +import React, { useState } from "react"; +import { useDispatch } from "react-redux"; +import { AppDispatch } from "@/redux/store.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; +const SignInRegisterForm: FlowStepComponent = ({ onSecondary, onPrimary }) => { + const dispatch: AppDispatch = useDispatch(); + const [userInput, setUserInput] = useState<string>(""); + const [validationError, setValidationError] = useState<string | null>(null); -const SignInRegisterForm: FlowStepComponent = ({onSecondary, onPrimary}) => { - const dispatch: AppDispatch = useDispatch(); - const [userInput, setUserInput] = useState<string>("") - const [validationError, setValidationError] = useState<string | null>(null); - - const validateInput = (input: string) => { - if (input.length < 6) { - setValidationError("Username must be at least 6 characters long."); - return false; - } - - setValidationError(null); - return true; - }; + const validateInput = (input: string) => { + if (input.length < 6) { + setValidationError("Username must be at least 6 characters long."); + return false; + } - const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setUserInput(event.target.value); - validateInput(event.target.value); - }; + setValidationError(null); + return true; + }; - const handleKeyDown = (event: React.KeyboardEvent) => { - // Example: Submit on Enter key - if (event.key === 'Enter' && validateInput(userInput)) { - handlePrimaryClick(); - } - }; + const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setUserInput(event.target.value); + validateInput(event.target.value); + }; - const handlePrimaryClick = () => { - if (validateInput(userInput)) { - dispatch(signinActions.updateSignInSessionField("username", userInput)); - onPrimary?.(); - } - }; + const handleKeyDown = (event: React.KeyboardEvent) => { + // Example: Submit on Enter key + if (event.key === "Enter" && validateInput(userInput)) { + handlePrimaryClick(); + } + }; - const handleSecondaryClick = () => { - onSecondary?.() + const handlePrimaryClick = () => { + if (validateInput(userInput)) { + dispatch(signinActions.updateSignInSessionField("username", userInput)); + onPrimary?.(); } + }; + + const handleSecondaryClick = () => { + onSecondary?.(); + }; - return ( - <> - <Card className="w-[700px]"> - <CardHeader> - <CardTitle>Registering User</CardTitle> - </CardHeader> - <CardContent> - <div className="relative"> - <Input - autoFocus={true} - type="text" - value={userInput} - onChange={handleInputChange} - onKeyDown={handleKeyDown} - placeholder="Enter your username..." - className="mb-2" - /> - {validationError && <p className="text-red-500">{validationError}</p>} - </div> - </CardContent> - <CardFooter className="flex justify-between flex-row-reverse"> - <Button onClick={handlePrimaryClick} disabled={!!validationError}>Continue</Button> - <Button onClick={handleSecondaryClick} variant="outline">Go Back</Button> - </CardFooter> - </Card> - </> -); + return ( + <> + <Card className="w-[700px]"> + <CardHeader> + <CardTitle>Registering User</CardTitle> + </CardHeader> + <CardContent> + <div className="relative"> + <Input + autoFocus={true} + type="text" + value={userInput} + onChange={handleInputChange} + onKeyDown={handleKeyDown} + placeholder="Enter your username..." + className="mb-2" + /> + {validationError && <p className="text-red-500">{validationError}</p>} + </div> + </CardContent> + <CardFooter className="flex justify-between flex-row-reverse"> + <Button onClick={handlePrimaryClick} disabled={!!validationError}> + Continue + </Button> + <Button onClick={handleSecondaryClick} variant="outline"> + Go Back + </Button> + </CardFooter> + </Card> + </> + ); }; export default SignInRegisterForm; diff --git a/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx b/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx index 674b3d9..e3a7970 100644 --- a/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx +++ b/apps/forge/src/components/signin/actions/SignOutDispatcher/index.tsx @@ -1,126 +1,125 @@ -import {Alert, AlertDescription, AlertTitle} from "@ui/components/ui/alert.tsx"; -import {ExclamationTriangleIcon} from "@radix-ui/react-icons"; -import {PostSignOut, PostSignOutProps} from "@/services/signin/signInService.ts"; -import {useMutation, useQueryClient} from "@tanstack/react-query"; -import {AppDispatch, AppRootState} from "@/redux/store.ts"; -import {useDispatch, useSelector} from "react-redux"; -import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; - -import {Loader} from "@ui/components/ui/loader.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {useEffect, useState} from "react"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {FlowStepComponent} from "@/components/signin/actions/SignInManager/types.ts"; -import {useNavigate} from "@tanstack/react-router"; - - - -const SignOutDispatcher: FlowStepComponent = ({onSecondary, onPrimary}) => { - const queryClient = useQueryClient(); - - const dispatch: AppDispatch = useDispatch(); - const signInSession = useSelector((state: AppRootState) => state.signin.session) - const activeLocation = useSelector((state: AppRootState) => state.signin.active_location) - const abortController = new AbortController(); // For gracefully cancelling the query - const [canContinue, setCanContinue] = useState<boolean>(false) - const navigate = useNavigate() - const timeout = 2000 - - const signOutProps: PostSignOutProps = { - locationName: activeLocation, - uCardNumber: signInSession?.ucard_number ?? 0, - signal: abortController.signal, - }; - - - const {isPending, error, mutate} = useMutation({ - mutationKey: ['postSignOut', signOutProps], - mutationFn: () => PostSignOut(signOutProps), - retry: 0, - onError: (error) => { - console.log('Error', error) - abortController.abort(); - }, - onSuccess: () => { - setCanContinue(true) - abortController.abort(); - redirectToActions(timeout) - queryClient.invalidateQueries({queryKey: ['locationStatus']}) - }, - }); - - const errorDisplay = (error: Error | null) => ( - <> - <Alert variant="destructive"> - <ExclamationTriangleIcon className="h-4 w-4"/> - <AlertTitle>Error</AlertTitle> - <AlertDescription> - There was an error with your session, try again! <br/> - Error: {error?.message ?? "Unknown"} - </AlertDescription> - </Alert> - </> - ) - - const redirectToActions = (timeoutInMs: number) => { - setTimeout(() => { - dispatch(signinActions.resetSignInSession()); - navigate({to: '/signin/actions'}) - }, timeoutInMs) +import { Alert, AlertDescription, AlertTitle } from "@ui/components/ui/alert.tsx"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { PostSignOut, PostSignOutProps } from "@/services/signin/signInService.ts"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { AppDispatch, AppRootState } from "@/redux/store.ts"; +import { useDispatch, useSelector } from "react-redux"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; + +import { Loader } from "@ui/components/ui/loader.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { useEffect, useState } from "react"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +import { useNavigate } from "@tanstack/react-router"; + +const SignOutDispatcher: FlowStepComponent = ({ onSecondary, onPrimary }) => { + const queryClient = useQueryClient(); + + const dispatch: AppDispatch = useDispatch(); + const signInSession = useSelector((state: AppRootState) => state.signin.session); + const activeLocation = useSelector((state: AppRootState) => state.signin.active_location); + const abortController = new AbortController(); // For gracefully cancelling the query + const [canContinue, setCanContinue] = useState<boolean>(false); + const navigate = useNavigate(); + const timeout = 2000; + + const signOutProps: PostSignOutProps = { + locationName: activeLocation, + uCardNumber: signInSession?.ucard_number ?? 0, + signal: abortController.signal, + }; + + const { isPending, error, mutate } = useMutation({ + mutationKey: ["postSignOut", signOutProps], + mutationFn: () => PostSignOut(signOutProps), + retry: 0, + onError: (error) => { + console.log("Error", error); + abortController.abort(); + }, + onSuccess: () => { + setCanContinue(true); + abortController.abort(); + redirectToActions(timeout); + queryClient.invalidateQueries({ queryKey: ["locationStatus"] }); + }, + }); + + const errorDisplay = (error: Error | null) => ( + <> + <Alert variant="destructive"> + <ExclamationTriangleIcon className="h-4 w-4" /> + <AlertTitle>Error</AlertTitle> + <AlertDescription> + There was an error with your session, try again! <br /> + Error: {error?.message ?? "Unknown"} + </AlertDescription> + </Alert> + </> + ); + + const redirectToActions = (timeoutInMs: number) => { + setTimeout(() => { + dispatch(signinActions.resetSignInSession()); + navigate({ to: "/signin/actions" }); + }, timeoutInMs); + }; + + const successDisplay = ( + <> + <div className="flex justify-items-center justify-center"> + <h1 className="text-xl flex-auto">Success!</h1> + <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> + </div> + </> + ); + + const handleSecondaryClick = () => { + abortController.abort(); + onSecondary?.(); + }; + + const handlePrimaryClick = () => { + if (canContinue) { + abortController.abort(); + onPrimary?.(); + console.log("Done "); + dispatch(signinActions.resetSignInSession()); } - - - const successDisplay = ( - <> - <div className="flex justify-items-center justify-center"> - <h1 className="text-xl flex-auto">Success!</h1> - <p className="text-sm">Possibly redirecting to actions page in ~{timeout / 1000} seconds...</p> - </div> - </> - ) - - const handleSecondaryClick = () => { - abortController.abort(); - onSecondary?.(); - } - - const handlePrimaryClick = () => { - if (canContinue) { - abortController.abort(); - onPrimary?.(); - console.log('Done ') - dispatch(signinActions.resetSignInSession()); - } - } - - useEffect(() => { - mutate() - }, [mutate]); - - - return ( - <> - <Card className="w-[700px]"> - <CardHeader> - <CardTitle>Signing Out</CardTitle> - </CardHeader> - <CardContent> - {!canContinue && !error && !isPending && ( - <Button onClick={() => mutate()} autoFocus={true} variant="default" - className="h-[200px] w-full">Sign Out</Button> - )} - {isPending && <Loader/>} - {!isPending && error && !canContinue && errorDisplay(error)} - {!isPending && canContinue && successDisplay} - </CardContent> - <CardFooter className="flex justify-between flex-row-reverse"> - <Button onClick={handlePrimaryClick} disabled={!canContinue}>Continue</Button> - <Button onClick={handleSecondaryClick} variant="outline">Go Back</Button> - </CardFooter> - </Card> - </> - ); + }; + + useEffect(() => { + mutate(); + }, [mutate]); + + return ( + <> + <Card className="w-[700px]"> + <CardHeader> + <CardTitle>Signing Out</CardTitle> + </CardHeader> + <CardContent> + {!canContinue && !error && !isPending && ( + <Button onClick={() => mutate()} autoFocus={true} variant="default" className="h-[200px] w-full"> + Sign Out + </Button> + )} + {isPending && <Loader />} + {!isPending && error && !canContinue && errorDisplay(error)} + {!isPending && canContinue && successDisplay} + </CardContent> + <CardFooter className="flex justify-between flex-row-reverse"> + <Button onClick={handlePrimaryClick} disabled={!canContinue}> + Continue + </Button> + <Button onClick={handleSecondaryClick} variant="outline"> + Go Back + </Button> + </CardFooter> + </Card> + </> + ); }; - -export default SignOutDispatcher \ No newline at end of file +export default SignOutDispatcher; diff --git a/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx b/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx index 44b8040..641bfcb 100644 --- a/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx +++ b/apps/forge/src/components/signin/actions/ToolSelectionInput/index.tsx @@ -1,7 +1,7 @@ import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; import { SelectedTrainingPipDisplay } from "@/components/signin/actions/ToolSelectionInput/SelectedTrainingPipDisplay.tsx"; import ToolSelectionList from "@/components/signin/actions/ToolSelectionInput/TrainingSelectionList.tsx"; -import { signinActions } from "@/redux/signin/signin.slice.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; import { AppDispatch, AppRootState } from "@/redux/store.ts"; import { GetSignIn, GetSignInProps } from "@/services/signin/signInService.ts"; import { Training, User } from "@ignis/types/sign_in.ts"; diff --git a/apps/forge/src/components/signin/actions/UCardInput/index.tsx b/apps/forge/src/components/signin/actions/UCardInput/index.tsx index 87d2d09..6779ccc 100644 --- a/apps/forge/src/components/signin/actions/UCardInput/index.tsx +++ b/apps/forge/src/components/signin/actions/UCardInput/index.tsx @@ -1,75 +1,81 @@ -import {useRef, useState} from "react"; -import OtpInput, {OtpInputHandle} from "@/components/signin/actions/UCardInput/input.tsx"; -import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {toast} from "sonner"; -import {useDispatch} from "react-redux"; -import {AppDispatch} from "@/redux/store.ts"; -import {signinActions} from "@/redux/signin/signin.slice.ts"; -import {FlowStepComponent} from "@/components/signin/actions/SignInManager/types.ts"; +import { useRef, useState } from "react"; +import OtpInput, { OtpInputHandle } from "@/components/signin/actions/UCardInput/input.tsx"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { toast } from "sonner"; +import { useDispatch } from "react-redux"; +import { AppDispatch } from "@/redux/store.ts"; +import { signinActions } from "@/redux/signin.slice.ts"; +import { FlowStepComponent } from "@/components/signin/actions/SignInManager/types.ts"; +const UCardInput: FlowStepComponent = ({ onPrimary }) => { + const [otp, setOtp] = useState<number>(0); + const [isOtpValid, setIsOtpValid] = useState<boolean>(false); + const otpRef = useRef<OtpInputHandle | null>(null); + const dispatch = useDispatch<AppDispatch>(); -const UCardInput: FlowStepComponent = ({onPrimary}) => { - const [otp, setOtp] = useState<number>(0); - const [isOtpValid, setIsOtpValid] = useState<boolean>(false); - const otpRef = useRef<OtpInputHandle | null>(null); - const dispatch = useDispatch<AppDispatch>(); + const UCARD_LENGTH = 9; + const STRIP_CHAR_AMOUNT = 3; + const VALID_LENGTH = UCARD_LENGTH - STRIP_CHAR_AMOUNT; - const UCARD_LENGTH = 9; - const STRIP_CHAR_AMOUNT = 3; - const VALID_LENGTH = UCARD_LENGTH - STRIP_CHAR_AMOUNT; - - const handleOtpChange = (value: number) => { - setOtp(value); - if (getNumberLength(value) === VALID_LENGTH) { - setIsOtpValid(true); - } - }; + const handleOtpChange = (value: number) => { + setOtp(value); + if (getNumberLength(value) === VALID_LENGTH) { + setIsOtpValid(true); + } + }; - const getNumberLength = (num: number): number => { - return num.toString().length; - }; + const getNumberLength = (num: number): number => { + return num.toString().length; + }; - const handleClear = () => { - console.log('Clearing OTP') - otpRef?.current?.clearOtp(); - }; + const handleClear = () => { + console.log("Clearing OTP"); + otpRef?.current?.clearOtp(); + }; - const handleOnSubmit = (otp: number) => { - if (getNumberLength(otp) === VALID_LENGTH) { - console.log('Submitting UCard:', otp) - toast(`UCard Entered: ${otp}`, { - description: `This is feedback that lets you know what the card has in fact been entered woop woop`, - action: { - label: "Woah", - onClick: () => console.log("ʕ •ᴥ• ʔ") - } - }); - dispatch(signinActions.updateSignInSessionField("ucard_number", otp)); - onPrimary?.(); - } + const handleOnSubmit = (otp: number) => { + if (getNumberLength(otp) === VALID_LENGTH) { + console.log("Submitting UCard:", otp); + toast(`UCard Entered: ${otp}`, { + description: `This is feedback that lets you know what the card has in fact been entered woop woop`, + action: { + label: "Woah", + onClick: () => console.log("ʕ •ᴥ• ʔ"), + }, + }); + dispatch(signinActions.updateSignInSessionField("ucard_number", otp)); + onPrimary?.(); } + }; - - return ( - <> - <Card className="w-[700px]"> - <CardHeader> - <CardTitle>UCard Input</CardTitle> - <CardDescription>Enter your UCard number</CardDescription> - </CardHeader> - <CardContent> - <OtpInput ref={otpRef} length={UCARD_LENGTH} stripChars={STRIP_CHAR_AMOUNT} - onOtpChange={handleOtpChange} onSubmit={handleOnSubmit}/> - </CardContent> - <CardFooter className="flex justify-between flex-row-reverse"> - <Button onClick={() => handleOnSubmit(otp)} disabled={!isOtpValid} aria-disabled={!isOtpValid}>Submit</Button> - <Button variant="outline" onClick={handleClear}>Clear</Button> - </CardFooter> - </Card> - </> - ); + return ( + <> + <Card className="w-[700px]"> + <CardHeader> + <CardTitle>UCard Input</CardTitle> + <CardDescription>Enter your UCard number</CardDescription> + </CardHeader> + <CardContent> + <OtpInput + ref={otpRef} + length={UCARD_LENGTH} + stripChars={STRIP_CHAR_AMOUNT} + onOtpChange={handleOtpChange} + onSubmit={handleOnSubmit} + /> + </CardContent> + <CardFooter className="flex justify-between flex-row-reverse"> + <Button onClick={() => handleOnSubmit(otp)} disabled={!isOtpValid} aria-disabled={!isOtpValid}> + Submit + </Button> + <Button variant="outline" onClick={handleClear}> + Clear + </Button> + </CardFooter> + </Card> + </> + ); }; -export default UCardInput - +export default UCardInput; diff --git a/apps/forge/src/config/constants.ts b/apps/forge/src/config/constants.ts index 1de5425..23c6858 100644 --- a/apps/forge/src/config/constants.ts +++ b/apps/forge/src/config/constants.ts @@ -1,7 +1,7 @@ -export const AUTH_SESSION_COOKIE = 'auth_session' +export const AUTH_SESSION_COOKIE = "auth_session"; -export const USER_EMAIL_DOMAIN = 'sheffield.ac.uk' +export const USER_EMAIL_DOMAIN = "sheffield.ac.uk"; -export const AUTH_COOKIE_EXPIRES = 7 +export const AUTH_COOKIE_EXPIRES = 7; -export const SIGN_IN_REASONS_STORAGE_KEY = 'sign_in_reasons' +export const SIGN_IN_REASONS_STORAGE_KEY = "sign_in_reasons"; diff --git a/apps/forge/src/config/nav.ts b/apps/forge/src/config/nav.ts new file mode 100644 index 0000000..98a16ee --- /dev/null +++ b/apps/forge/src/config/nav.ts @@ -0,0 +1,14 @@ +import { Apps } from "@/types/app.ts"; + +interface AppNavMapping { + [routeSegment: string]: Apps; +} + +export const appNavMapping: AppNavMapping = { + auth: "Auth", + training: "Training", + "": "Main", + users: "User", + printing: "Printing", + signin: "Sign In", +}; diff --git a/apps/forge/src/hooks/useCurrentApp.ts b/apps/forge/src/hooks/useCurrentApp.ts new file mode 100644 index 0000000..e167f99 --- /dev/null +++ b/apps/forge/src/hooks/useCurrentApp.ts @@ -0,0 +1,28 @@ +import { useState, useEffect } from "react"; +import { useMatchRoute, useRouterState } from "@tanstack/react-router"; +import { Apps } from "@/types/app.ts"; +import { appRoutes } from "@/types/common.ts"; +import { appNavMapping } from "@/config/nav.ts"; + +const useCurrentApp = (): Apps | undefined => { + const [currentApp, setCurrentApp] = useState<Apps | undefined>(undefined); + const matchRoute = useMatchRoute(); + + const routerChanged = useRouterState().isTransitioning; + + // biome-ignore lint/correctness/useExhaustiveDependencies: Needed to include router state or else this doesn't update quick enough (side effect is it updates too much)> + useEffect(() => { + for (const [routeSegment, app] of Object.entries(appNavMapping)) { + const route = routeSegment as appRoutes; + const match = matchRoute({ to: `/${route}`, fuzzy: true }); + if (match) { + setCurrentApp(app); + return; // Stop the loop once a match is found + } + } + }, [matchRoute, routerChanged]); + + return currentApp; +}; + +export default useCurrentApp; diff --git a/apps/forge/src/hooks/useLogout.ts b/apps/forge/src/hooks/useLogout.ts new file mode 100644 index 0000000..c828606 --- /dev/null +++ b/apps/forge/src/hooks/useLogout.ts @@ -0,0 +1,35 @@ +import { AppDispatch, persistor } from "@/redux/store.ts"; +import { authActions } from "@/redux/auth.slice.ts"; +import { userActions } from "@/redux/user.slice.ts"; +import axiosInstance from "@/api/axiosInstance.ts"; +import { toast } from "sonner"; +import { useNavigate } from "@tanstack/react-router"; +import { useAuth } from "@/components/auth-provider"; +import { useDispatch } from "react-redux"; +import axios from "axios"; + +export const useLogout = () => { + const dispatch: AppDispatch = useDispatch(); + const auth = useAuth(); + const navigate = useNavigate(); + + return async () => { + try { + await axiosInstance.post("/authentication/logout"); + await persistor.purge(); + dispatch(userActions.clearUser()); + dispatch(authActions.onLogout()); + auth.logout(); + toast.success("Logged out successfully."); + await navigate({ to: "/" }); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 409) { + await navigate({ to: "/" }); + return; + } + console.error("Logout failed:", error); + toast.error("Logout failed."); + await navigate({ to: "/" }); + } + }; +}; diff --git a/apps/forge/src/hooks/useVerifyAuthentication.ts b/apps/forge/src/hooks/useVerifyAuthentication.ts new file mode 100644 index 0000000..7f2f644 --- /dev/null +++ b/apps/forge/src/hooks/useVerifyAuthentication.ts @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { User } from "@ignis/types/users.ts"; +import axiosInstance from "@/api/axiosInstance.ts"; +import { userActions } from "@/redux/user.slice.ts"; +import { authActions } from "@/redux/auth.slice.ts"; + +export const useVerifyAuthentication = () => { + const [loading, setLoading] = useState(true); + const [user, setUser] = useState<User | null>(null); + const dispatch = useDispatch(); + + const verifyAuthentication = useCallback(async () => { + setLoading(true); + try { + const response = await axiosInstance.get("/users/me"); + if (response.status === 200) { + dispatch(userActions.setUser(response.data)); + dispatch(authActions.onLogin()); + setUser(response.data); + } else { + setUser(null); + dispatch(authActions.onLogout()); + } + } catch (error) { + setUser(null); + dispatch(authActions.onLogout()); + } finally { + setLoading(false); + } + }, [dispatch]); + + useEffect(() => { + verifyAuthentication(); + }, [verifyAuthentication]); + + return { user, loading, setUser }; +}; diff --git a/apps/forge/src/hooks/useVerifyAuthentication.tsx b/apps/forge/src/hooks/useVerifyAuthentication.tsx deleted file mode 100644 index d5a2c44..0000000 --- a/apps/forge/src/hooks/useVerifyAuthentication.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import {useCallback, useEffect, useState} from "react"; -import {useDispatch} from "react-redux"; -import {User} from "@ignis/types/users.ts"; -import axiosInstance from "@/api/axiosInstance.ts"; -import {userActions} from "@/redux/user/user.slice.ts"; -import {authActions} from "@/redux/auth/auth.slice.ts"; - -export const useVerifyAuthentication = () => { - const [loading, setLoading] = useState(true); - const [user, setUser] = useState<User | null>(null); - const dispatch = useDispatch(); - - const verifyAuthentication = useCallback(async () => { - setLoading(true); - try { - const response = await axiosInstance.get("/users/me"); - if (response.status === 200) { - dispatch(userActions.setUser(response.data)); - dispatch(authActions.onLogin()); - setUser(response.data); - } else { - setUser(null); - dispatch(authActions.onLogout()); - } - } catch (error) { - setUser(null); - dispatch(authActions.onLogout()); - } finally { - setLoading(false); - } - }, [dispatch]); - - useEffect(() => { - verifyAuthentication(); - }, [verifyAuthentication]); - - return { user, loading, setUser}; -}; \ No newline at end of file diff --git a/apps/forge/src/redux/app/app.slice.ts b/apps/forge/src/redux/app/app.slice.ts deleted file mode 100644 index b1664d1..0000000 --- a/apps/forge/src/redux/app/app.slice.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { RESET_APP } from "@/redux/common/common.types"; -import { Apps, AppState } from "@/redux/app/app.types"; - - -const initialState: AppState = { - current_app: "Main", - is_loading: false -}; - -const appSlice = createSlice({ - name: "app", - initialState: initialState, - reducers: { - setApp: (state, action: PayloadAction<Apps>) => { - state.current_app = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(RESET_APP, () => initialState); - }, -}); - -export const { actions: appActions, reducer: appReducer } = appSlice; -export const initialAppState = initialState; diff --git a/apps/forge/src/redux/app/app.types.ts b/apps/forge/src/redux/app/app.types.ts deleted file mode 100644 index 17ae74d..0000000 --- a/apps/forge/src/redux/app/app.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Apps = "Main" | "Sign In" | "Printing" | "Social" | "Admin" | "Training"; - -export interface AppState { - current_app: Apps; - is_loading: boolean; -} diff --git a/apps/forge/src/redux/auth/auth.slice.ts b/apps/forge/src/redux/auth.slice.ts similarity index 79% rename from apps/forge/src/redux/auth/auth.slice.ts rename to apps/forge/src/redux/auth.slice.ts index e860a39..12c1075 100644 --- a/apps/forge/src/redux/auth/auth.slice.ts +++ b/apps/forge/src/redux/auth.slice.ts @@ -1,7 +1,6 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { RESET_APP } from '@/redux/common/common.types'; -import { AuthState } from '@/redux/auth/auth.types'; - +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { RESET_APP } from "@/types/common.ts"; +import { AuthState } from "@/types/auth.ts"; // Define initial state based on persisted state or default values const initialState: AuthState = { @@ -10,7 +9,7 @@ const initialState: AuthState = { }; const authSlice = createSlice({ - name: 'auth', + name: "auth", initialState: initialState, reducers: { setIsAuthenticated: (state, action: PayloadAction<boolean>) => { @@ -20,7 +19,7 @@ const authSlice = createSlice({ state.is_loading = action.payload; }, setRedirect: (state, action: PayloadAction<string>) => { - state.redirect = action.payload + state.redirect = action.payload; }, onLogin: (state) => { state.is_authenticated = true; @@ -37,4 +36,4 @@ const authSlice = createSlice({ }, }); -export const { actions: authActions, reducer: authReducer } = authSlice; \ No newline at end of file +export const { actions: authActions, reducer: authReducer } = authSlice; diff --git a/apps/forge/src/redux/common/common.types.ts b/apps/forge/src/redux/common/common.types.ts deleted file mode 100644 index f98a1af..0000000 --- a/apps/forge/src/redux/common/common.types.ts +++ /dev/null @@ -1 +0,0 @@ -export const RESET_APP = "RESET_APP"; diff --git a/apps/forge/src/redux/signin/signin.slice.ts b/apps/forge/src/redux/signin.slice.ts similarity index 79% rename from apps/forge/src/redux/signin/signin.slice.ts rename to apps/forge/src/redux/signin.slice.ts index eb0cbfd..7b0b8d4 100644 --- a/apps/forge/src/redux/signin/signin.slice.ts +++ b/apps/forge/src/redux/signin.slice.ts @@ -1,11 +1,18 @@ -import { RESET_APP } from "@/redux/common/common.types"; -import { defaultSignInState } from "@/redux/signin/signin.defaults.ts"; -import { SignInSession } from "@/redux/signin/signin.types.ts"; -import { LocationStatus, Reason, Training } from "@ignis/types/sign_in.ts"; +import { RESET_APP } from "@/types/common.ts"; +import { SignInSession, SignInState } from "@/types/signin.ts"; +import { Location, LocationStatus, Reason, Training } from "@ignis/types/sign_in.ts"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; type SignInSessionFieldValue = number | null | boolean | Reason | string | Training[]; +const defaultSignInState: SignInState = { + active_location: "mainspace", + is_loading: false, + error: "", + locations: [], + session: null, +}; + interface UpdateSignInSessionFieldPayload { field: keyof SignInSession; value: SignInSessionFieldValue; @@ -17,7 +24,7 @@ const signinSlice = createSlice({ name: "signin", initialState: initialState, reducers: { - setActiveLocation: (state, action: PayloadAction<string>) => { + setActiveLocation: (state, action: PayloadAction<Location>) => { state.active_location = action.payload; }, setIsLoading: (state, action: PayloadAction<boolean>) => { diff --git a/apps/forge/src/redux/signin/signin.defaults.ts b/apps/forge/src/redux/signin/signin.defaults.ts deleted file mode 100644 index 56be4ac..0000000 --- a/apps/forge/src/redux/signin/signin.defaults.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {SignInState} from "@/redux/signin/signin.types.ts"; - -export const defaultSignInState: SignInState = { - active_location: "", - is_loading: false, - error: "", - locations: [], - session: null -} diff --git a/apps/forge/src/redux/store.ts b/apps/forge/src/redux/store.ts index 18616bf..cef4d96 100644 --- a/apps/forge/src/redux/store.ts +++ b/apps/forge/src/redux/store.ts @@ -1,74 +1,62 @@ -import {combineReducers, configureStore} from '@reduxjs/toolkit'; -import {appReducer} from '@/redux/app/app.slice'; -import {authReducer} from '@/redux/auth/auth.slice'; -import {userReducer} from '@/redux/user/user.slice'; -import storage from 'redux-persist/lib/storage'; -import {persistReducer, persistStore} from 'redux-persist'; -import {signinReducer} from "@/redux/signin/signin.slice.ts"; -import {AppState} from "@/redux/app/app.types.ts"; -import {AuthState} from "@/redux/auth/auth.types.ts"; -import {UserState} from "@/redux/user/user.types.ts"; -import {SignInState} from "@/redux/signin/signin.types.ts"; +import { combineReducers, configureStore } from "@reduxjs/toolkit"; +import { authReducer } from "@/redux/auth.slice.ts"; +import { userReducer } from "@/redux/user.slice.ts"; +import storage from "redux-persist/lib/storage"; +import { persistReducer, persistStore } from "redux-persist"; +import { signinReducer } from "@/redux/signin.slice.ts"; +import { AuthState } from "@/types/auth.ts"; +import { UserState } from "@/types/user.ts"; +import { SignInState } from "@/types/signin.ts"; export interface AppRootState { - app: AppState - auth: AuthState, - user: UserState, - signin: SignInState, + auth: AuthState; + user: UserState; + signin: SignInState; } const rootReducer = combineReducers({ - app: appReducer, - auth: authReducer, - user: userReducer, - signin: signinReducer, + auth: authReducer, + user: userReducer, + signin: signinReducer, }); - const persistConfig = { - key: 'root', - storage, - whitelist: ['user', 'app', 'signin'], - version: 1, + key: "root", + storage, + whitelist: ["user", "app", "signin"], + version: 1, }; const persistedReducer = persistReducer(persistConfig, rootReducer); // Define Store export const store = configureStore({ - reducer: persistedReducer, - // WARN This could maybe be investigated to see if ignoring it is fully okay - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - serializableCheck: { - ignoredActions: ['persist/PERSIST'], - }, - }), + reducer: persistedReducer, + // WARN This could maybe be investigated to see if ignoring it is fully okay + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ["persist/PERSIST"], + }, + }), }); export const persistor = persistStore(store); if (import.meta.hot) { - import.meta.hot.accept( - [ - "./user/user.slice", - "./app/app.slice", - "./auth/auth.slice", - "./signin/signin.slice", - ], - async () => { - const newRootReducer = combineReducers({ - user: (await import("./user/user.slice")).userReducer, - app: (await import("./app/app.slice")).appReducer, - auth: (await import("./auth/auth.slice")).authReducer, - signin: (await import("./signin/signin.slice")).signinReducer, - }); - - store.replaceReducer(persistReducer(persistConfig, newRootReducer)); - } - ); + import.meta.hot.accept( + ["./user/user.slice", "./app/app.slice", "./auth/auth.slice", "./signin/signin.slice"], + async () => { + const newRootReducer = combineReducers({ + user: (await import("./user.slice.ts")).userReducer, + auth: (await import("./auth.slice.ts")).authReducer, + signin: (await import("./signin.slice.ts")).signinReducer, + }); + + store.replaceReducer(persistReducer(persistConfig, newRootReducer)); + }, + ); } - export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; diff --git a/apps/forge/src/redux/user/user.slice.ts b/apps/forge/src/redux/user.slice.ts similarity index 78% rename from apps/forge/src/redux/user/user.slice.ts rename to apps/forge/src/redux/user.slice.ts index e0c9161..30c5bd7 100644 --- a/apps/forge/src/redux/user/user.slice.ts +++ b/apps/forge/src/redux/user.slice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { RESET_APP } from "@/redux/common/common.types"; -import { UserState } from "@/redux/user/user.types.ts"; -import { User } from "@ignis/types/users"; +import { RESET_APP } from "@/types/common.ts"; +import { UserState } from "@/types/user.ts"; +import { User } from "@ignis/types/users.ts"; const initialState: UserState = { user: null, diff --git a/apps/forge/src/routes/__root.tsx b/apps/forge/src/routes/__root.tsx index 624c3af..7e1589c 100644 --- a/apps/forge/src/routes/__root.tsx +++ b/apps/forge/src/routes/__root.tsx @@ -1,72 +1,41 @@ -import { - createRootRouteWithContext, - Outlet, - ScrollRestoration, - useNavigate, -} from "@tanstack/react-router"; -import {QueryClient} from "@tanstack/react-query"; +import { createRootRouteWithContext, Outlet, ScrollRestoration } from "@tanstack/react-router"; +import { QueryClient } from "@tanstack/react-query"; import NavBar from "@/components/navbar"; import CommandMenu from "@/components/command-menu"; -import React, {Suspense} from "react"; -import {AuthContext} from "@/components/auth-provider"; -import Title from "@/components/title"; -import { Button } from "@ui/components/ui/button"; +import React, { Suspense } from "react"; +import { AuthContext } from "@/components/auth-provider"; +import { NotFound } from "@/components/routing/NotFound.tsx"; +import { GenericError } from "@/components/routing/GenericError.tsx"; const TanStackRouterDevtools = - process.env.NODE_ENV === 'production' - ? () => null // Render nothing in production - : React.lazy(() => - // Lazy load in development - import('@tanstack/router-devtools').then((res) => ({ - default: res.TanStackRouterDevtools, - })), - ) + process.env.NODE_ENV === "production" + ? () => null // Render nothing in production + : React.lazy(() => + // Lazy load in development + import("@tanstack/router-devtools").then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ); function RootComponent() { - return ( - <> - <NavBar/> - <ScrollRestoration/> - <CommandMenu/> - <Outlet/> {/* This is where child routes will render */} - <Suspense> - <TanStackRouterDevtools/> - </Suspense> - </> - ); + return ( + <> + <NavBar /> + <ScrollRestoration /> + <CommandMenu /> + <Outlet /> {/* This is where child routes will render */} + <Suspense> + <TanStackRouterDevtools /> + </Suspense> + </> + ); } -function NotFoundComponent() { - const navigate = useNavigate(); - return ( - <> - <RootComponent/> - <Title prompt="Not Found" /> - <div className="flex items-center justify-center w-full min-h-[80vh] px-4"> - <div className="grid items-center gap-4 text-center"> - <div className="space-y-2"> - <h1 className="text-4xl font-bold tracking-tighter sm:text-6xl">404 - Page Not Found</h1> - <p className="max-w-[600px] mx-auto text-accent-foreground md:text-xl/relaxed"> - Oops! The page you are looking for does not exist or is under construction. - </p> - <p className="text-sm text-accent-foreground"> - Note: This site is still under development. For assistance, join our <a className="text-primary" href={import.meta.env.VITE_DISCORD_URL}>Discord server</a>. - </p> - </div> - <div className="flex justify-center"> - <Button variant="outline" onClick={() => {navigate({to: "/"})}}>Go to Homepage</Button> - </div> - </div> - </div> - </> - ) -} - - export const Route = createRootRouteWithContext<{ - queryClient: QueryClient; - auth: AuthContext; + queryClient: QueryClient; + auth: AuthContext; }>()({ - component: RootComponent, - notFoundComponent: NotFoundComponent, + component: RootComponent, + notFoundComponent: NotFound, + errorComponent: GenericError, }); diff --git a/apps/forge/src/routes/_authenticated/printing/index.tsx b/apps/forge/src/routes/_authenticated/printing/index.tsx index 0ef2c5b..d9da742 100644 --- a/apps/forge/src/routes/_authenticated/printing/index.tsx +++ b/apps/forge/src/routes/_authenticated/printing/index.tsx @@ -1,16 +1,16 @@ -import {createFileRoute} from "@tanstack/react-router"; -import Title from "../../../components/title"; +import { createFileRoute } from "@tanstack/react-router"; +import Title from "@/components/title"; +import { RouteUnfinished } from "@/components/routing/RouteUnfinished.tsx"; const PrintingAppIndexComponent = () => { + return ( + <> + <Title prompt="Printing App" /> + <div className="p-2"> + <h3>Printing App</h3> + </div> + </> + ); +}; - return ( - <> - <Title prompt="Printing App"/> - <div className="p-2"> - <h3>Printing App</h3> - </div> - </> - ) -} - -export const Route = createFileRoute('/_authenticated/printing/')({component: PrintingAppIndexComponent}) \ No newline at end of file +export const Route = createFileRoute("/_authenticated/printing/")({ component: RouteUnfinished }); diff --git a/apps/forge/src/routes/_authenticated/user/profile/index.tsx b/apps/forge/src/routes/_authenticated/user/profile/index.tsx index 6a89b93..2acdb60 100644 --- a/apps/forge/src/routes/_authenticated/user/profile/index.tsx +++ b/apps/forge/src/routes/_authenticated/user/profile/index.tsx @@ -1,5 +1,6 @@ import Title from "@/components/title"; import { createFileRoute } from "@tanstack/react-router"; +import { RouteUnfinished } from "@/components/routing/RouteUnfinished.tsx"; const UserProfilePageComponent = () => { return ( @@ -13,5 +14,5 @@ const UserProfilePageComponent = () => { }; export const Route = createFileRoute("/_authenticated/user/profile/")({ - component: UserProfilePageComponent, + component: RouteUnfinished, }); diff --git a/apps/forge/src/routes/_authenticated/user/settings/index.tsx b/apps/forge/src/routes/_authenticated/user/settings/index.tsx index 3bc793f..344699e 100644 --- a/apps/forge/src/routes/_authenticated/user/settings/index.tsx +++ b/apps/forge/src/routes/_authenticated/user/settings/index.tsx @@ -1,17 +1,16 @@ import Title from "@/components/title"; -import {createFileRoute} from "@tanstack/react-router"; - +import { createFileRoute } from "@tanstack/react-router"; +import { RouteUnfinished } from "@/components/routing/RouteUnfinished.tsx"; const UserSettingsPageComponent = () => { + return ( + <> + <Title prompt="Settings" /> + <div className="p-2"> + <h3>USER SETTINGS PAGE</h3> + </div> + </> + ); +}; - return ( - <> - <Title prompt="Settings"/> - <div className="p-2"> - <h3>USER SETTINGS PAGE</h3> - </div> - </> - ) -} - -export const Route = createFileRoute('/_authenticated/user/settings/')({component: UserSettingsPageComponent}) +export const Route = createFileRoute("/_authenticated/user/settings/")({ component: RouteUnfinished }); diff --git a/apps/forge/src/routes/auth/login/complete.tsx b/apps/forge/src/routes/auth/login/complete.tsx index 48acc28..9211f6c 100644 --- a/apps/forge/src/routes/auth/login/complete.tsx +++ b/apps/forge/src/routes/auth/login/complete.tsx @@ -1,31 +1,28 @@ -import { AppRootState } from '@/redux/store'; -import { Navigate, createFileRoute } from '@tanstack/react-router' -import {useDispatch, useSelector} from 'react-redux'; -import {Loader} from "@ui/components/ui/loader.tsx"; -import {useVerifyAuthentication} from "@/hooks/useVerifyAuthentication.tsx"; -import {userActions} from "@/redux/user/user.slice.ts"; - +import { AppRootState } from "@/redux/store"; +import { Navigate, createFileRoute } from "@tanstack/react-router"; +import { useDispatch, useSelector } from "react-redux"; +import { Loader } from "@ui/components/ui/loader.tsx"; +import { useVerifyAuthentication } from "@/hooks/useVerifyAuthentication.ts"; +import { userActions } from "@/redux/user.slice.ts"; export const CompleteComponent = () => { - const { user, loading } = useVerifyAuthentication(); - const dispatch = useDispatch(); - const redirect = useSelector((state: AppRootState) => state.auth.redirect) || "/"; - - if (loading) { - return <Loader />; - } + const { user, loading } = useVerifyAuthentication(); + const dispatch = useDispatch(); + const redirect = useSelector((state: AppRootState) => state.auth.redirect) || "/"; - if (!user) { - return <Navigate to="/auth/login" replace={true} />; - } + if (loading) { + return <Loader />; + } - dispatch(userActions.setUser(user)) + if (!user) { + return <Navigate to="/auth/login" replace={true} />; + } + dispatch(userActions.setUser(user)); - return <Navigate to={redirect} replace={true} />; + return <Navigate to={redirect} replace={true} />; }; - -export const Route = createFileRoute('/auth/login/complete')({ - component: CompleteComponent -}) \ No newline at end of file +export const Route = createFileRoute("/auth/login/complete")({ + component: CompleteComponent, +}); diff --git a/apps/forge/src/routes/auth/login/index.tsx b/apps/forge/src/routes/auth/login/index.tsx index 41f026b..7e11f1c 100644 --- a/apps/forge/src/routes/auth/login/index.tsx +++ b/apps/forge/src/routes/auth/login/index.tsx @@ -1,46 +1,41 @@ -import {createFileRoute, Link, useSearch} from "@tanstack/react-router"; -import React, {useEffect} from "react"; -import {AppDispatch} from "@/redux/store.ts"; -import {useDispatch} from "react-redux"; -import {authActions} from "@/redux/auth/auth.slice.ts"; +import { createFileRoute, Link, useSearch } from "@tanstack/react-router"; +import React, { useEffect } from "react"; +import { AppDispatch } from "@/redux/store.ts"; +import { useDispatch } from "react-redux"; +import { authActions } from "@/redux/auth.slice.ts"; import Title from "@/components/title"; -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@ui/components/ui/card.tsx"; -import {Button} from "@ui/components/ui/button.tsx"; -import {EnvelopeOpenIcon} from "@radix-ui/react-icons"; - +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/components/ui/card.tsx"; +import { Button } from "@ui/components/ui/button.tsx"; +import { EnvelopeOpenIcon } from "@radix-ui/react-icons"; const Index: React.FC = () => { - const dispatch: AppDispatch = useDispatch(); - const {redirect = "/"}: {redirect: string} = useSearch({strict: false}); + const dispatch: AppDispatch = useDispatch(); + const { redirect = "/" }: { redirect: string } = useSearch({ strict: false }); - useEffect(() => { - dispatch(authActions.setRedirect(redirect)); - }, []); + useEffect(() => { + dispatch(authActions.setRedirect(redirect)); + }, []); - return ( - <> - <Title prompt="Login" /> - <Card className="max-w-md mx-auto mt-10 p-6 rounded-xl shadow-md space-y-4"> - <CardHeader> - <CardTitle className="text-2xl text-center">Login</CardTitle> - <CardDescription className="text-center"> - Please chose a method to login with - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - <Link to={`${import.meta.env.VITE_API_URL}/authentication/login`} className="flex justify-center"> - <Button> - <EnvelopeOpenIcon className="mr-2 h-4 w-4" /> Login with Google - </Button> - </Link> - </CardContent> - </Card> - </> - ); + return ( + <> + <Title prompt="Login" /> + <Card className="max-w-md mx-auto mt-10 p-6 rounded-xl shadow-md space-y-4"> + <CardHeader> + <CardTitle className="text-2xl text-center">Login</CardTitle> + <CardDescription className="text-center">Please chose a method to login with</CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <Link to={`${import.meta.env.VITE_API_URL}/authentication/login`} className="flex justify-center"> + <Button> + <EnvelopeOpenIcon className="mr-2 h-4 w-4" /> Login with Google + </Button> + </Link> + </CardContent> + </Card> + </> + ); }; - -export const Route = createFileRoute('/auth/login/')({ - component: Index, -}) - +export const Route = createFileRoute("/auth/login/")({ + component: Index, +}); diff --git a/apps/forge/src/routes/auth/logout/index.tsx b/apps/forge/src/routes/auth/logout/index.tsx index c442bc6..7e4fd6b 100644 --- a/apps/forge/src/routes/auth/logout/index.tsx +++ b/apps/forge/src/routes/auth/logout/index.tsx @@ -1,26 +1,38 @@ -import {createFileRoute} from "@tanstack/react-router"; +import { useEffect } from "react"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import Title from "@/components/title"; -import {useDispatch} from "react-redux"; -import {AppDispatch} from "@/redux/store.ts"; -import {logoutService} from "@/services/auth/logoutService.ts"; +import { useLogout } from "@/hooks/useLogout.ts"; const LogOutComponent = () => { - const dispatch: AppDispatch = useDispatch(); + const logout = useLogout(); + const navigate = useNavigate(); - // Call the logout service - logoutService.logout(dispatch); + useEffect(() => { + const performLogout = async () => { + try { + await logout(); + } catch (error) { + // handle errors if any + console.error("Failed to logout:", error); + navigate({ to: "/" }); + } + }; + performLogout(); + }, [logout, navigate]); - return ( - <> - <Title prompt="Logout"/> - <div className="p-2"> - <h3>log out!!!!!!!</h3> - </div> - </> - ) -} + return ( + <> + <Title prompt="Logout" /> + <div className="p-2"> + <h3>Logging out...</h3> + </div> + </> + ); +}; +export default LogOutComponent; -export const Route = createFileRoute('/auth/logout/')({ - component: LogOutComponent}); +export const Route = createFileRoute("/auth/logout/")({ + component: LogOutComponent, +}); diff --git a/apps/forge/src/services/auth/loginService.ts b/apps/forge/src/services/auth/loginService.ts deleted file mode 100644 index f778371..0000000 --- a/apps/forge/src/services/auth/loginService.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {USER_EMAIL_DOMAIN} from "@/config/constants.ts"; -import axiosInstance from "@/api/axiosInstance"; -import { User } from "@ignis/types/users"; - -export type LoginResponse = { - user: User; - access_token: string; - refresh_token: string; -}; - -export async function login( - username: string, - password: string -): Promise<LoginResponse> { - const response = await axiosInstance.post("/authentication/ldap-login", { - username, - password, - }); - const user = response.data.user; - user.email = `${user.email}@${USER_EMAIL_DOMAIN}`; - return response.data; -} diff --git a/apps/forge/src/services/auth/logoutService.ts b/apps/forge/src/services/auth/logoutService.ts deleted file mode 100644 index 50a4931..0000000 --- a/apps/forge/src/services/auth/logoutService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {AppDispatch, persistor} from "@/redux/store.ts"; -import { authActions } from "@/redux/auth/auth.slice.ts"; -import {userActions} from "@/redux/user/user.slice.ts"; -import axiosInstance from "@/api/axiosInstance.ts"; -import {toast} from "sonner"; -import {useNavigate} from "@tanstack/react-router"; -import {useAuth} from "@/components/auth-provider"; - -const logout = async (dispatch: AppDispatch) => { - const auth = useAuth(); - const navigate = useNavigate() - try { - await axiosInstance.post('/authentication/logout'); - - await persistor.purge(); - dispatch(userActions.clearUser()); - dispatch(authActions.onLogout()); - auth.logout() - // stops component from hanging here and trying again - toast.success("Logged out successfully."); - await navigate({to: "/"}); - } catch (error) { - console.error('Logout failed:', error); - toast.error("Logout failed."); - - } - await navigate({to: "/"}); -}; - -export const logoutService = { - logout, -}; \ No newline at end of file diff --git a/apps/forge/src/types/app.ts b/apps/forge/src/types/app.ts new file mode 100644 index 0000000..62c3462 --- /dev/null +++ b/apps/forge/src/types/app.ts @@ -0,0 +1 @@ +export type Apps = "Main" | "Sign In" | "Printing" | "User" | "Admin" | "Training" | "Auth"; diff --git a/apps/forge/src/redux/auth/auth.types.ts b/apps/forge/src/types/auth.ts similarity index 100% rename from apps/forge/src/redux/auth/auth.types.ts rename to apps/forge/src/types/auth.ts diff --git a/apps/forge/src/types/common.ts b/apps/forge/src/types/common.ts new file mode 100644 index 0000000..bee9500 --- /dev/null +++ b/apps/forge/src/types/common.ts @@ -0,0 +1,6 @@ +import { RoutePaths } from "@tanstack/react-router"; +import { routeTree } from "@/routeTree.gen.ts"; + +export const RESET_APP = "RESET_APP"; + +export type appRoutes = RoutePaths<typeof routeTree>; diff --git a/apps/forge/src/redux/signin/signin.types.ts b/apps/forge/src/types/signin.ts similarity index 100% rename from apps/forge/src/redux/signin/signin.types.ts rename to apps/forge/src/types/signin.ts diff --git a/apps/forge/src/redux/user/user.types.ts b/apps/forge/src/types/user.ts similarity index 62% rename from apps/forge/src/redux/user/user.types.ts rename to apps/forge/src/types/user.ts index d7f9bdb..2019ba6 100644 --- a/apps/forge/src/redux/user/user.types.ts +++ b/apps/forge/src/types/user.ts @@ -1,4 +1,4 @@ -import { User } from "@ignis/types/users"; +import { User } from "@ignis/types/users.ts"; export interface UserState { user: User | null;