From 2438a07eb765033fbe8608ea773212a8c2936337 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sat, 25 Jul 2026 01:03:16 -0400 Subject: [PATCH] Added NotificationProvider --- app/components/notification.tsx | 33 ++++++ app/hooks/auth.tsx | 171 +++++++++++++++++--------------- app/hooks/notification.tsx | 62 ++++++++++++ app/root.tsx | 120 +++++++++++----------- 4 files changed, 246 insertions(+), 140 deletions(-) create mode 100644 app/components/notification.tsx create mode 100644 app/hooks/notification.tsx diff --git a/app/components/notification.tsx b/app/components/notification.tsx new file mode 100644 index 0000000..b119a52 --- /dev/null +++ b/app/components/notification.tsx @@ -0,0 +1,33 @@ +import { Alert, Spinner } from "@heroui/react"; + +export type NotificationType = { + type: "success" | "default" | "accent" | "danger" | "warning"; + loading: boolean; + title: string; + message: string; +}; + +export function Notification({ + type, + loading, + title, + message, +}: NotificationType) { + return ( +
+ + {loading ? ( + + + + ) : ( + + )} + + {title} + {message} + + +
+ ); +} diff --git a/app/hooks/auth.tsx b/app/hooks/auth.tsx index ef542ec..92b8a6d 100644 --- a/app/hooks/auth.tsx +++ b/app/hooks/auth.tsx @@ -1,4 +1,3 @@ -import { Alert, Spinner } from "@heroui/react"; import { useEffect, useState, @@ -7,6 +6,7 @@ import { type PropsWithChildren, } from "react"; import { useLocation, useSearchParams } from "react-router"; +import { useNotification } from "./notification"; type User = { id: string; @@ -33,56 +33,96 @@ export function AuthProvider({ children }: PropsWithChildren) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [searchParams, setSeachParams] = useSearchParams(); - const [popupShowed, setPopupShowed] = useState(false); - const [error, setError] = useState(""); + const { notify } = useNotification(); const location = useLocation(); - useEffect(() => { - if (user !== null && !loading) { - const timer = setTimeout(() => { - setPopupShowed(true); - }, 2000); - return () => clearTimeout(timer); - } else if (error !== "") { - const timer = setTimeout(() => { - setPopupShowed(true); - }, 10000); - return () => clearTimeout(timer); - } - else { - setPopupShowed(false); - } - }, [user, loading, error]); - useEffect(() => { const load = async () => { - try { if (searchParams.get("openid.ns") !== null) { setLoading(true); + notify( + { + type: "accent", + loading: true, + title: "Logging in...", + message: + "Please wait while we log you in and sync your data. This may take a few moments.", + }, + Infinity, + ); let query = searchParams.toString(); setSeachParams({}, { replace: true }); let response = await fetch( - "http://localhost:8080/login?" + query, + `${import.meta.env.VITE_BASE_API_URL}/login?${query}`, { credentials: "include" }, - ).then((r) => r.text()); + ) + .then((r) => r.text()) + .catch((e) => { + notify( + { + type: "danger", + loading: false, + title: "An error occured.", + message: `An error occured: ${e}`, + }, + 10000, + ); + }); if (response === "nok") { - setError("An error occured during the information exchange between Steam and Porning.") + notify( + { + type: "danger", + loading: false, + title: "An error occured.", + message: + "An error occured during the information exchange between Steam and Porning.", + }, + 10000, + ); } } - let data = await fetch("http://localhost:8080/me", { + let data = await fetch(`${import.meta.env.VITE_BASE_API_URL}/me`, { credentials: "include", - }).then((r) => r.json()); + }) + .then((r) => r.json()) + .catch((e) => { + notify( + { + type: "danger", + loading: false, + title: "An error occured.", + message: `An error occured: ${e}`, + }, + 10000, + ); + }); if (data === null) { - setError("An error occured while trying to fetch your user profile. Please contact an administrator.") + notify( + { + type: "danger", + loading: false, + title: "An error occured.", + message: + "An error occured while trying to fetch your user profile. Please contact an administrator.", + }, + 10000, + ); + } else if (data !== undefined) { + setUser(data); + notify( + { + type: "success", + loading: false, + title: "Login successful!", + message: "", + }, + 2000, + ); } - setUser(data); setLoading(false); - } catch (_) { - setError("An error occured while communicating with the server.") - } }; load(); @@ -90,65 +130,32 @@ export function AuthProvider({ children }: PropsWithChildren) { const login = () => { window.location.replace( - "https://steamcommunity.com/openid/login?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=http://localhost:5173&openid.return_to=http://localhost:5173" + - location.pathname, + `https://steamcommunity.com/openid/login?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${import.meta.env.VITE_BASE_URL}&openid.return_to=${import.meta.env.VITE_BASE_URL}${location.pathname}`, ); }; const logout = async () => { - await fetch("http://localhost:8080/logout", { credentials: "include" }); - setUser(null); + await fetch(import.meta.env.VITE_BASE_API_URL + "/logout", { + credentials: "include", + }) + .then(() => { + setUser(null); + }) + .catch((e) => { + notify( + { + type: "danger", + loading: false, + title: "An error occured.", + message: `An error occured: ${e}`, + }, + 10000, + ); + }); }; return ( - {loading ? ( -
- - - - - - Logging in... - - Please wait while we log you in and sync your - data. This may take a few moments. - - - -
- ) : ( - (!popupShowed && - user !== null && ( -
- - - - Login sucessful! - - -
- )) || (!popupShowed && error !== "" && ( -
- - - - An error occured. - {error} - - -
- )) - )} {children}
); diff --git a/app/hooks/notification.tsx b/app/hooks/notification.tsx new file mode 100644 index 0000000..080650d --- /dev/null +++ b/app/hooks/notification.tsx @@ -0,0 +1,62 @@ +import { + createContext, + type PropsWithChildren, + useEffect, + useState, + useContext, +} from "react"; +import { type NotificationType, Notification } from "~/components/notification"; + +type NotificationContextType = { + notify: (arg0: NotificationType, arg1: number) => void; +}; + +const NotificationContext = createContext({ + notify: (_arg0: NotificationType, _arg1: number) => {}, +}); + +export function NotificationProvider({ children }: PropsWithChildren) { + const [notification, setNotification] = useState( + null, + ); + const [notificationTimeout, setNotificationTimeout] = useState(2000); + const [popupShowed, setPopupShowed] = useState(false); + + const notify = (newNotification: NotificationType, timeout: number) => { + console.log("Changed notification"); + setNotification(newNotification); + setNotificationTimeout(timeout); + }; + + useEffect(() => { + if (!popupShowed && notification !== null) { + console.log("setting timeout") + const timeout = setTimeout(() => { + console.log("timeout done") + setPopupShowed(true); + setNotification(null); + }, notificationTimeout); + return () => clearTimeout(timeout); + } else { + setPopupShowed(false); + } + }, [notification, notificationTimeout]); + + return ( + + {notification !== null && !popupShowed && ( + + )} + {children} + + ); +} + +export function useNotification() { + return useContext(NotificationContext); +} diff --git a/app/root.tsx b/app/root.tsx index 9c96f36..4dc0adb 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,78 +1,82 @@ import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, } from "react-router"; import type { Route } from "./+types/root"; import "./app.css"; import { AuthProvider } from "./hooks/auth"; +import { NotificationProvider } from "./hooks/notification"; export const links: Route.LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, ]; export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - - {children} - - - - - - ); + return ( + + + + + + + + + + {children} + + + + + + ); } export default function App() { - return ; + return ; } export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = "Oops!"; - let details = "An unexpected error occurred."; - let stack: string | undefined; + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? "404" : "Error"; - details = - error.status === 404 - ? "The requested page could not be found." - : error.statusText || details; - } else if (import.meta.env.DEV && error && error instanceof Error) { - details = error.message; - stack = error.stack; - } + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } - return ( -
-

{message}

-

{details}

- {stack && ( -
-          {stack}
-        
- )} -
- ); + return ( +
+

{message}

+

{details}

+ {stack && ( +
+                    {stack}
+                
+ )} +
+ ); }