Added NotificationProvider
This commit is contained in:
33
app/components/notification.tsx
Normal file
33
app/components/notification.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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", {
|
||||||
setUser(null);
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
62
app/hooks/notification.tsx
Normal file
62
app/hooks/notification.tsx
Normal 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);
|
||||||
|
}
|
||||||
120
app/root.tsx
120
app/root.tsx
@@ -1,78 +1,82 @@
|
|||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
Links,
|
Links,
|
||||||
Meta,
|
Meta,
|
||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
|
|
||||||
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" },
|
||||||
{
|
{
|
||||||
rel: "preconnect",
|
rel: "preconnect",
|
||||||
href: "https://fonts.gstatic.com",
|
href: "https://fonts.gstatic.com",
|
||||||
crossOrigin: "anonymous",
|
crossOrigin: "anonymous",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "stylesheet",
|
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",
|
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 }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<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
|
||||||
<Meta />
|
name="viewport"
|
||||||
<Links />
|
content="width=device-width, initial-scale=1"
|
||||||
</head>
|
/>
|
||||||
<body className="bg-background text-foreground">
|
<Meta />
|
||||||
<AuthProvider>
|
<Links />
|
||||||
{children}
|
</head>
|
||||||
</AuthProvider>
|
<body className="bg-background text-foreground">
|
||||||
<ScrollRestoration />
|
<NotificationProvider>
|
||||||
<Scripts />
|
<AuthProvider>{children}</AuthProvider>
|
||||||
</body>
|
</NotificationProvider>
|
||||||
</html>
|
<ScrollRestoration />
|
||||||
);
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
let message = "Oops!";
|
let message = "Oops!";
|
||||||
let details = "An unexpected error occurred.";
|
let details = "An unexpected error occurred.";
|
||||||
let stack: string | undefined;
|
let stack: string | undefined;
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
message = error.status === 404 ? "404" : "Error";
|
message = error.status === 404 ? "404" : "Error";
|
||||||
details =
|
details =
|
||||||
error.status === 404
|
error.status === 404
|
||||||
? "The requested page could not be found."
|
? "The requested page could not be found."
|
||||||
: error.statusText || details;
|
: error.statusText || details;
|
||||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
details = error.message;
|
details = error.message;
|
||||||
stack = error.stack;
|
stack = error.stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="pt-16 p-4 container mx-auto">
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
<h1>{message}</h1>
|
<h1>{message}</h1>
|
||||||
<p>{details}</p>
|
<p>{details}</p>
|
||||||
{stack && (
|
{stack && (
|
||||||
<pre className="w-full p-4 overflow-x-auto">
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
<code>{stack}</code>
|
<code>{stack}</code>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user