2 Commits

10 changed files with 137 additions and 4 deletions
+25
View File
@@ -39,6 +39,7 @@
"@tanstack/react-query": "^5.96.1", "@tanstack/react-query": "^5.96.1",
"@tanstack/react-query-devtools": "^5.100.9", "@tanstack/react-query-devtools": "^5.100.9",
"axios": "^1.14.0", "axios": "^1.14.0",
"axios-retry": "^4.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -3769,6 +3770,18 @@
"proxy-from-env": "^2.1.0" "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": { "node_modules/bail": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -5695,6 +5708,18 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+1
View File
@@ -44,6 +44,7 @@
"@tanstack/react-query": "^5.96.1", "@tanstack/react-query": "^5.96.1",
"@tanstack/react-query-devtools": "^5.100.9", "@tanstack/react-query-devtools": "^5.100.9",
"axios": "^1.14.0", "axios": "^1.14.0",
"axios-retry": "^4.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
+1 -1
View File
@@ -1,6 +1,5 @@
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { AnimatePresence } from "framer-motion"; import { AnimatePresence } from "framer-motion";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
@@ -17,6 +16,7 @@ import Projects from "./pages/Projects";
import Technologies from "./pages/Technologies"; import Technologies from "./pages/Technologies";
import { QueryProvider } from "./provider/QueryProvider"; import { QueryProvider } from "./provider/QueryProvider";
import OverviewPage from "./components/admin/dashboards/OverviewPage"; import OverviewPage from "./components/admin/dashboards/OverviewPage";
import { ThemeProvider } from "./provider/ThemeProvider";
function AnimatedRoutes() { function AnimatedRoutes() {
const location = useLocation(); const location = useLocation();
+33 -2
View File
@@ -1,11 +1,42 @@
// src/api/axiosInstance.ts
import axios from "axios"; import axios from "axios";
import axiosRetry from "axios-retry";
import { API_URL } from "./config";
import {
requestInterceptor,
responseSuccessInterceptor,
responseErrorInterceptor,
} from "./interceptors";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: API_URL,
timeout: 5000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json", "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; export default axiosInstance;
+9
View File
@@ -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;
+38
View File
@@ -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,
};
}
+21
View File
@@ -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);
}
+5
View File
@@ -0,0 +1,5 @@
export interface ApiError {
status: number;
message: string;
code?: string;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/hooks/useTheme";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Moon, PanelLeftClose, PanelLeftOpen, Sun } from "lucide-react"; import { Moon, PanelLeftClose, PanelLeftOpen, Sun } from "lucide-react";
+3
View File
@@ -0,0 +1,3 @@
export function getAccessToken() {
return localStorage.getItem("accessToken");
}