diff --git a/package-lock.json b/package-lock.json index d464793..183437f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@tanstack/react-query": "^5.96.1", "@tanstack/react-query-devtools": "^5.100.9", "axios": "^1.14.0", + "axios-retry": "^4.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -3769,6 +3770,18 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -5695,6 +5708,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", diff --git a/package.json b/package.json index 38ec427..b68fdfb 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@tanstack/react-query": "^5.96.1", "@tanstack/react-query-devtools": "^5.100.9", "axios": "^1.14.0", + "axios-retry": "^4.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src/App.tsx b/src/App.tsx index da9ab18..3e3cc81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { ThemeProvider } from "@/contexts/ThemeContext"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { AnimatePresence } from "framer-motion"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; @@ -17,6 +16,7 @@ import Projects from "./pages/Projects"; import Technologies from "./pages/Technologies"; import { QueryProvider } from "./provider/QueryProvider"; import OverviewPage from "./components/admin/dashboards/OverviewPage"; +import { ThemeProvider } from "./provider/ThemeProvider"; function AnimatedRoutes() { const location = useLocation(); diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 52cbe91..761ae13 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -1,11 +1,42 @@ +// src/api/axiosInstance.ts + import axios from "axios"; +import axiosRetry from "axios-retry"; + +import { API_URL } from "./config"; + +import { + requestInterceptor, + responseSuccessInterceptor, + responseErrorInterceptor, +} from "./interceptors"; const axiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_URL, - timeout: 5000, + baseURL: API_URL, + timeout: 10000, headers: { "Content-Type": "application/json", }, }); +axiosInstance.interceptors.request.use( + requestInterceptor +); + +axiosInstance.interceptors.response.use( + responseSuccessInterceptor, + responseErrorInterceptor +); + +axiosRetry(axiosInstance, { + retries: 3, + retryDelay: axiosRetry.exponentialDelay, + + retryCondition: (error) => + axiosRetry.isNetworkOrIdempotentRequestError( + error + ) || + error.response?.status === 429, +}); + export default axiosInstance; \ No newline at end of file diff --git a/src/api/config.ts b/src/api/config.ts new file mode 100644 index 0000000..15c4d6a --- /dev/null +++ b/src/api/config.ts @@ -0,0 +1,9 @@ +const apiUrl = import.meta.env.VITE_API_URL; + +if (!apiUrl) { + throw new Error( + "Missing VITE_API_URL environment variable" + ); +} + +export const API_URL = apiUrl; \ No newline at end of file diff --git a/src/api/errors.ts b/src/api/errors.ts new file mode 100644 index 0000000..8ef5ab5 --- /dev/null +++ b/src/api/errors.ts @@ -0,0 +1,38 @@ +// src/api/errors.ts + +import axios from "axios"; +import { ApiError } from "./types"; + +export function normalizeApiError(error: unknown): ApiError { + if (!axios.isAxiosError(error)) { + return { + status: 0, + message: "Unexpected error occurred", + }; + } + + if (error.code === "ECONNABORTED") { + return { + status: 408, + message: "Request timeout", + }; + } + + if (!error.response) { + return { + status: 0, + message: "Network error", + }; + } + + const { status, data } = error.response; + + return { + status, + message: + data?.message || + data?.error || + "Something went wrong", + code: data?.code, + }; +} \ No newline at end of file diff --git a/src/api/interceptors.ts b/src/api/interceptors.ts new file mode 100644 index 0000000..d71979d --- /dev/null +++ b/src/api/interceptors.ts @@ -0,0 +1,21 @@ +import { getAccessToken } from "@/lib/auth"; +import { AxiosResponse, InternalAxiosRequestConfig } from "axios"; +import { normalizeApiError } from "./errors"; + +export function requestInterceptor(config: InternalAxiosRequestConfig) { + const token = getAccessToken(); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; +} + +export const responseSuccessInterceptor = (response: AxiosResponse) => response; + +export function responseErrorInterceptor(error: unknown) { + const normalizedError = normalizeApiError(error); + + return Promise.reject(normalizedError); +} diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..2b6fb9e --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,5 @@ +export interface ApiError { + status: number; + message: string; + code?: string; +} \ No newline at end of file diff --git a/src/components/admin/dashboards/Topbar.tsx b/src/components/admin/dashboards/Topbar.tsx index 25daa0b..2e1ce36 100644 --- a/src/components/admin/dashboards/Topbar.tsx +++ b/src/components/admin/dashboards/Topbar.tsx @@ -1,4 +1,4 @@ -import { useTheme } from "@/contexts/ThemeContext"; +import { useTheme } from "@/hooks/useTheme"; import { motion, AnimatePresence } from "framer-motion"; import { Moon, PanelLeftClose, PanelLeftOpen, Sun } from "lucide-react"; diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..d9d712d --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,3 @@ +export function getAccessToken() { + return localStorage.getItem("accessToken"); +} \ No newline at end of file