Added NotificationProvider

This commit is contained in:
2026-07-25 01:03:16 -04:00
parent d0975354af
commit 2438a07eb7
4 changed files with 246 additions and 140 deletions

View File

@@ -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 (
<div className="flex justify-center fixed top-12 w-dvw z-50">
<Alert className="w-fit h-fit border border-white" status={type}>
{loading ? (
<Alert.Indicator>
<Spinner size="sm" />
</Alert.Indicator>
) : (
<Alert.Indicator />
)}
<Alert.Content>
<Alert.Title>{title}</Alert.Title>
<Alert.Description>{message}</Alert.Description>
</Alert.Content>
</Alert>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import { Alert, Spinner } from "@heroui/react";
import { import {
useEffect, useEffect,
useState, useState,
@@ -7,6 +6,7 @@ import {
type PropsWithChildren, type PropsWithChildren,
} from "react"; } from "react";
import { useLocation, useSearchParams } from "react-router"; import { useLocation, useSearchParams } from "react-router";
import { useNotification } from "./notification";
type User = { type User = {
id: string; id: string;
@@ -33,56 +33,96 @@ export function AuthProvider({ children }: PropsWithChildren) {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchParams, setSeachParams] = useSearchParams(); const [searchParams, setSeachParams] = useSearchParams();
const [popupShowed, setPopupShowed] = useState(false); const { notify } = useNotification();
const [error, setError] = useState("");
const location = useLocation(); 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(() => { useEffect(() => {
const load = async () => { const load = async () => {
try {
if (searchParams.get("openid.ns") !== null) { if (searchParams.get("openid.ns") !== null) {
setLoading(true); 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(); let query = searchParams.toString();
setSeachParams({}, { replace: true }); setSeachParams({}, { replace: true });
let response = await fetch( let response = await fetch(
"http://localhost:8080/login?" + query, `${import.meta.env.VITE_BASE_API_URL}/login?${query}`,
{ credentials: "include" }, { 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") { 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", 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) { 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); setLoading(false);
} catch (_) {
setError("An error occured while communicating with the server.")
}
}; };
load(); load();
@@ -90,65 +130,32 @@ export function AuthProvider({ children }: PropsWithChildren) {
const login = () => { const login = () => {
window.location.replace( 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" + `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}`,
location.pathname,
); );
}; };
const logout = async () => { const logout = async () => {
await fetch("http://localhost:8080/logout", { credentials: "include" }); await fetch(import.meta.env.VITE_BASE_API_URL + "/logout", {
credentials: "include",
})
.then(() => {
setUser(null); setUser(null);
})
.catch((e) => {
notify(
{
type: "danger",
loading: false,
title: "An error occured.",
message: `An error occured: ${e}`,
},
10000,
);
});
}; };
return ( return (
<AuthContext.Provider value={{ user, loading, login, logout }}> <AuthContext.Provider value={{ user, loading, login, logout }}>
{loading ? (
<div className="flex justify-center fixed top-12 w-dvw z-50">
<Alert
className="w-fit h-fit border border-white"
status="accent"
>
<Alert.Indicator>
<Spinner size="sm" />
</Alert.Indicator>
<Alert.Content>
<Alert.Title>Logging in...</Alert.Title>
<Alert.Description>
Please wait while we log you in and sync your
data. This may take a few moments.
</Alert.Description>
</Alert.Content>
</Alert>
</div>
) : (
(!popupShowed &&
user !== null && (
<div className="flex justify-center fixed top-12 w-dvw z-50">
<Alert
className="w-fit h-fit border border-white"
status="success"
>
<Alert.Indicator />
<Alert.Content>
<Alert.Title>Login sucessful!</Alert.Title>
</Alert.Content>
</Alert>
</div>
)) || (!popupShowed && error !== "" && (
<div className="flex justify-center fixed top-12 w-dvw z-50">
<Alert
className="w-fit h-fit border border-white"
status="danger"
>
<Alert.Indicator />
<Alert.Content>
<Alert.Title>An error occured.</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Content>
</Alert>
</div>
))
)}
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@@ -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<NotificationContextType>({
notify: (_arg0: NotificationType, _arg1: number) => {},
});
export function NotificationProvider({ children }: PropsWithChildren) {
const [notification, setNotification] = useState<NotificationType | null>(
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 (
<NotificationContext.Provider value={{ notify }}>
{notification !== null && !popupShowed && (
<Notification
type={notification.type}
loading={notification.loading}
title={notification.title}
message={notification.message}
/>
)}
{children}
</NotificationContext.Provider>
);
}
export function useNotification() {
return useContext(NotificationContext);
}

View File

@@ -10,6 +10,7 @@ import {
import type { Route } from "./+types/root"; import type { Route } from "./+types/root";
import "./app.css"; import "./app.css";
import { AuthProvider } from "./hooks/auth"; import { AuthProvider } from "./hooks/auth";
import { NotificationProvider } from "./hooks/notification";
export const links: Route.LinksFunction = () => [ export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.googleapis.com" },
@@ -29,14 +30,17 @@ export function Layout({ children }: { children: React.ReactNode }) {
<html className="dark" data-theme="dark" lang="en"> <html className="dark" data-theme="dark" lang="en">
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta /> <Meta />
<Links /> <Links />
</head> </head>
<body className="bg-background text-foreground"> <body className="bg-background text-foreground">
<AuthProvider> <NotificationProvider>
{children} <AuthProvider>{children}</AuthProvider>
</AuthProvider> </NotificationProvider>
<ScrollRestoration /> <ScrollRestoration />
<Scripts /> <Scripts />
</body> </body>