import { type DefaultError, type UseMutationOptions, type queryOptions, useQuery } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" import { useEffect } from "react" enum ApiErrorCode { BadRequestBody = "BadRequestBody", WebsiteUnreachable = "WebsiteUnreachable", UnsupportedWebsite = "UnsupportedWebsite", } class BadRequestError extends Error { constructor( public readonly code: ApiErrorCode, public readonly message: string = "Server returned status 400", ) { super() } } class InternalError extends Error {} class UnauthenticatedError extends Error {} interface ErrorBody { code: string message?: string } type QueryKey = ["bookmarks", ...ReadonlyArray] | ["bookmarks", string, ...ReadonlyArray] async function fetchApi(route: string, init?: RequestInit): Promise { const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, { credentials: "include", ...init, }) switch (response.status) { case 200: case 204: return response case 400: { const body: ErrorBody = await response.json() throw new BadRequestError(body.code as ApiErrorCode, body.message) } case 401: { throw new UnauthenticatedError() } default: throw new InternalError() } } function useAuthenticatedQuery(queryKey: QueryKey, fn: () => Promise) { const query = useQuery({ queryKey, queryFn: () => fn(), retry: (_, error) => !(error instanceof UnauthenticatedError), }) const navigate = useNavigate() useEffect(() => { if (query.error && query.error instanceof UnauthenticatedError) { navigate({ to: "/login", replace: true }) } }, [query.error, navigate]) return query } function mutationOptions( options: UseMutationOptions, ): UseMutationOptions { return options } export { ApiErrorCode, BadRequestError, InternalError, UnauthenticatedError, fetchApi, useAuthenticatedQuery, mutationOptions, } export type { QueryKey }