Created api hook to reduce boilerplate code

This commit is contained in:
2026-09-28 23:07:24 +02:00
parent d89b679d08
commit b1d57c645b
3 changed files with 109 additions and 58 deletions

88
app/hooks/api.tsx Normal file
View File

@@ -0,0 +1,88 @@
import {
useState,
createContext,
useContext,
type PropsWithChildren,
} from "react";
import { useNotification } from "./notification";
import type { NotificationType } from "~/components/notification";
type ApiContextType = {
get: (arg0: string) => Promise<any>;
post: (arg0: string, arg1: any) => Promise<void>;
};
const apiContext = createContext<ApiContextType>({
get: async (_: string) => {},
post: async (_a: string, _b: any) => {},
});
export function ApiProvider({ children }: PropsWithChildren) {
const { notify } = useNotification();
const get = async (endpoint: string) => {
let data = undefined;
let response = await fetch(
`${import.meta.env.VITE_BASE_API_URL}/${endpoint}`,
{ credentials: "include" },
).catch((e) => notifyError(notify, e.code, e.message));
if (!response) {
notifyError(notify, 1, "No data in response from server.");
} else if (!response.ok) {
notifyError(notify, response.status, await response.text());
} else {
const text = await response.text();
data = text ? JSON.parse(text): null;
}
return data;
};
const post = async (endpoint: string, data: any) => {
let response = await fetch(
`${import.meta.env.VITE_BASE_API_URL}/${endpoint}`,
{
credentials: "include",
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
},
).catch((e) => notifyError(notify, e.code, e.message));
if (!response) {
notifyError(notify, 1, "No data in response from server.");
} else if (!response.ok) {
notifyError(notify, response.status, await response.text());
}
};
return (
<apiContext.Provider value={{ get, post }}>
{children}
</apiContext.Provider>
);
}
export function useApi() {
return useContext(apiContext);
}
function notifyError(
notify: (arg0: NotificationType, arg1: number) => void,
code: number,
message: string,
) {
notify(
{
type: "danger",
loading: false,
title: "Oops, something went wrong.",
message: `There was an issue on our end: ${message} (Error ${code})`,
},
10000,
);
}

View File

@@ -7,6 +7,7 @@ import {
} from "react"; } from "react";
import { useLocation, useSearchParams } from "react-router"; import { useLocation, useSearchParams } from "react-router";
import { useNotification } from "./notification"; import { useNotification } from "./notification";
import { useApi } from "./api";
type User = { type User = {
id: string; id: string;
@@ -39,6 +40,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
const [searchParams, setSeachParams] = useSearchParams(); const [searchParams, setSeachParams] = useSearchParams();
const { notify } = useNotification(); const { notify } = useNotification();
const location = useLocation(); const location = useLocation();
const { get } = useApi();
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
@@ -56,22 +58,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
); );
let query = searchParams.toString(); let query = searchParams.toString();
setSeachParams({}, { replace: true }); setSeachParams({}, { replace: true });
let response = await fetch( await get(`login?${query}`);
`${import.meta.env.VITE_BASE_API_URL}/login?${query}`,
{ credentials: "include" },
)
.then((r) => r.text())
.catch((e) => {
notify(
{
type: "danger",
loading: false,
title: "An error occured.",
message: `An error occured: ${e}`,
},
10000,
);
});
} }
if (await update()) { if (await update()) {
@@ -98,50 +85,23 @@ export function AuthProvider({ children }: PropsWithChildren) {
}; };
const logout = async () => { const logout = async () => {
await fetch(import.meta.env.VITE_BASE_API_URL + "/logout", { const data = await get("logout");
credentials: "include", if (data === null) {
}) setUser(null);
.then(() => { notify(
setUser(null); {
notify( type: "success",
{ loading: false,
type: "success", title: "Logout successful!",
loading: false, message: "",
title: "Logout successful!", },
message: "", 2000,
}, );
2000, }
);
})
.catch((e) => {
notify(
{
type: "danger",
loading: false,
title: "An error occured.",
message: `An error occured: ${e}`,
},
10000,
);
});
}; };
const update = async () => { const update = async () => {
let data = await fetch(`${import.meta.env.VITE_BASE_API_URL}/me`, { let data = await get("me");
credentials: "include",
})
.then((r) => r.json())
.catch((e) => {
notify(
{
type: "danger",
loading: false,
title: "An error occured.",
message: `An error occured: ${e}`,
},
10000,
);
});
if (data !== undefined) { if (data !== undefined) {
setUser(data); setUser(data);

View File

@@ -11,6 +11,7 @@ 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"; import { NotificationProvider } from "./hooks/notification";
import { ApiProvider } from "./hooks/api";
export const links: Route.LinksFunction = () => [ export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.googleapis.com" },
@@ -39,7 +40,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
</head> </head>
<body className="bg-background text-foreground"> <body className="bg-background text-foreground">
<NotificationProvider> <NotificationProvider>
<AuthProvider>{children}</AuthProvider> <ApiProvider>
<AuthProvider>{children}</AuthProvider>
</ApiProvider>
</NotificationProvider> </NotificationProvider>
<ScrollRestoration /> <ScrollRestoration />
<Scripts /> <Scripts />