diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7c6fb4f --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=/api/v1 + diff --git a/.gitignore b/.gitignore index a547bf3..3b0b403 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/README.md b/README.md index a36934d..61aa2cd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ -# React + Vite +# Listhub Frontend Repository -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +To start developing -Currently, two official plugins are available: +```bash +npm install +``` -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +and then -## React Compiler +```bash +npm run start +``` -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). -## Expanding the ESLint configuration +## API configuration -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +The frontend reads the base backend URL from `VITE_API_URL` in `src/config/api.js`. + +- Default: `/api/v1` (works with the Vite dev proxy) +- To override it, create a local `.env` file and set `VITE_API_URL` to your backend URL + +See `.env.example` for a ready-to-copy example. diff --git a/src/api/api.js b/src/api/api.js new file mode 100644 index 0000000..cc4bbea --- /dev/null +++ b/src/api/api.js @@ -0,0 +1,10 @@ +import axios from "axios"; +import { API_URL } from "../config/api"; + +const api = axios.create({ + baseURL: API_URL, +}); + +export { api }; +export default api; + diff --git a/src/api/auth.js b/src/api/auth.js index 04850e0..1101359 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -1,10 +1,8 @@ -import axios from "axios"; - -export const API_URL = "/api/v1"; +import api from "./api"; export async function login(userData) { try { - const response = await axios.post(`${API_URL}/auth/login`, userData, { + const response = await api.post(`/auth/login`, userData, { headers: {"Content-Type": "application/json"}, }); return response.data; @@ -16,7 +14,7 @@ export async function login(userData) { export async function register(userData) { try { - const response = await axios.post(`${API_URL}/auth/register`, userData, { + const response = await api.post(`/auth/register`, userData, { headers: {"Content-Type": "application/json"}, }); return response.data; @@ -28,8 +26,8 @@ export async function register(userData) { export async function googleLogin(googleToken) { try { - const response = await axios.post( - `${API_URL}/auth/google`, + const response = await api.post( + `/auth/google`, { googleToken: googleToken }, { headers: { "Content-Type": "application/json" }, @@ -45,8 +43,8 @@ export async function googleLogin(googleToken) { export async function logout(token) { const headers = token ? { Authorization: `Bearer ${token}` } : {}; try { - const response = await axios.post( - `${API_URL}/auth/logout`, + const response = await api.post( + `/auth/logout`, {}, { headers: headers, diff --git a/src/api/categories.js b/src/api/categories.js index 2e58b38..0c8bb9c 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,12 +1,10 @@ -import axios from "axios"; import { useAuthStore } from "../store/authStore"; - -const API_URL = "/api/v1"; +import api from "./api"; export async function listCategories() { const { token } = useAuthStore.getState(); const headers = token ? { Authorization: `Bearer ${token}` } : {}; - const response = await axios.get(`${API_URL}/vars/categories`, { headers }); + const response = await api.get(`/vars/categories`, { headers }); return Array.isArray(response.data) ? response.data : []; } diff --git a/src/api/notices.js b/src/api/notices.js index af4c657..5781e36 100644 --- a/src/api/notices.js +++ b/src/api/notices.js @@ -1,18 +1,16 @@ -import axios from "axios"; import { useAuthStore } from "../store/authStore"; - -const API_URL = "/api/v1"; +import api from "./api"; const getAuthHeaders = () => { const { token } = useAuthStore.getState(); return token ? { Authorization: `Bearer ${token}` } : {}; }; -const buildImageUrl = (imageName) => `${API_URL}/images/get/${imageName}`; +const buildImageUrl = (imageName) => `/images/get/${imageName}`; const fetchImageAsBlobUrl = async (imageUrl) => { const headers = getAuthHeaders(); - const response = await axios.get(imageUrl, { + const response = await api.get(imageUrl, { headers, responseType: "blob", }); @@ -22,27 +20,18 @@ const fetchImageAsBlobUrl = async (imageUrl) => { export async function listNotices() { const headers = getAuthHeaders(); - const response = await fetch(`${API_URL}/notices/get/all`, { + const response = await api.get(`/notices/get/all`, { headers: headers, }); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to fetch notices"); - } - return data; + return response.data; } export async function getNoticeById(noticeId) { const headers = getAuthHeaders(); - const response = await fetch(`${API_URL}/notices/get/${noticeId}`, { + const response = await api.get(`/notices/get/${noticeId}`, { headers, }); - const data = await response.json(); - if (!response.ok) { - throw new Error("Error fetching notice"); - } - return data; + return response.data; } export async function createNotice(notice) { @@ -58,7 +47,7 @@ export async function createNotice(notice) { }; try { - const response = await axios.post(`${API_URL}/notices/add`, payload, { + const response = await api.post(`/notices/add`, payload, { headers: headers, }); @@ -82,7 +71,7 @@ export async function getImageByNoticeId(noticeId) { const headers = getAuthHeaders(); try { - const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`, { + const listResponse = await api.get(`/images/list/${noticeId}`, { headers, }); const imageName = listResponse.data[0]; @@ -98,7 +87,7 @@ export async function getImageByNoticeId(noticeId) { export async function getAllImagesByNoticeId(noticeId) { const headers = getAuthHeaders(); try { - const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`, { + const listResponse = await api.get(`/images/list/${noticeId}`, { headers: headers, }); @@ -126,8 +115,8 @@ export const uploadImage = async (noticeId, file, isFirst) => { formData.append("file", file); try { - const response = await axios.post( - `${API_URL}/images/upload/${noticeId}?isMainImage=${isFirst}`, + const response = await api.post( + `/images/upload/${noticeId}?isMainImage=${isFirst}`, formData, { headers: headers } ); @@ -140,8 +129,8 @@ export const uploadImage = async (noticeId, file, isFirst) => { export const deleteNotice = async (noticeId) => { const headers = getAuthHeaders(); - const response = await axios.delete( - `${API_URL}/notices/delete/${noticeId}`, + const response = await api.delete( + `/notices/delete/${noticeId}`, { headers: headers } ); return response.data; @@ -149,8 +138,8 @@ export const deleteNotice = async (noticeId) => { export const editNotice = async (noticeId, notice) => { const headers = getAuthHeaders(); - const response = await axios.put( - `${API_URL}/notices/edit/${noticeId}`, + const response = await api.put( + `/notices/edit/${noticeId}`, { title: notice.title, description: notice.description, @@ -176,8 +165,8 @@ export const editNotice = async (noticeId, notice) => { export const deleteImage = async (filename) => { const headers = getAuthHeaders(); - const response = await axios.delete( - `${API_URL}/images/delete/${filename}`, + const response = await api.delete( + `/images/delete/${filename}`, { headers: headers } ); return response.data; @@ -187,7 +176,7 @@ export async function listImageNamesByNoticeId(noticeId) { const headers = getAuthHeaders(); try { - const response = await axios.get(`${API_URL}/images/list/${noticeId}`, { + const response = await api.get(`/images/list/${noticeId}`, { headers, }); return Array.isArray(response.data) ? response.data : []; diff --git a/src/api/wishlist.js b/src/api/wishlist.js index 9379ec9..3411602 100644 --- a/src/api/wishlist.js +++ b/src/api/wishlist.js @@ -1,15 +1,13 @@ -import axios from "axios"; import { useAuthStore } from "../store/authStore"; - -const API_URL = "/api/v1/wishlist"; +import api from "./api"; export async function toggleNoticeStatus(noticeId) { const { token } = useAuthStore.getState(); const headers = token ? { Authorization: `Bearer ${token}` } : {}; try { - const response = await axios.post( - `${API_URL}/toggle/${noticeId}`, + const response = await api.post( + `/wishlist/toggle/${noticeId}`, {}, { headers: headers } ); @@ -25,7 +23,7 @@ export async function getWishlist() { const headers = token ? { Authorization: `Bearer ${token}` } : {}; try { - const response = await axios.get(`${API_URL}/`, { headers: headers }); + const response = await api.get(`/wishlist/`, { headers: headers }); return response.data; } catch (error) { console.error("Error fetching wishlist:", error); diff --git a/src/config/api.js b/src/config/api.js new file mode 100644 index 0000000..4556183 --- /dev/null +++ b/src/config/api.js @@ -0,0 +1,8 @@ +const DEFAULT_API_URL = "/api/v1"; + +const trimTrailingSlash = (value) => value.replace(/\/+$/, ""); + +export const API_URL = trimTrailingSlash( + import.meta.env.VITE_API_URL || DEFAULT_API_URL +); + diff --git a/src/store/authStore.js b/src/store/authStore.js index 6468a02..80ae756 100644 --- a/src/store/authStore.js +++ b/src/store/authStore.js @@ -1,26 +1,42 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import axios from "axios"; import * as api from "../api/auth"; +import apiClient from "../api/api"; + +let authInterceptorId = null; + +const shouldRedirectToLogin = (error) => { + const status = Number(error?.response?.status); + return status === 401 || status === 403; +}; + +const registerAuthInterceptor = (set) => { + if (authInterceptorId !== null) { + return; + } + + authInterceptorId = apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (!shouldRedirectToLogin(error)) { + return Promise.reject(error); + } + + set({ user_id: null, token: null, isLoading: false }); + delete apiClient.defaults.headers.common["Authorization"]; + + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + return Promise.reject(error); + } + ); +}; export const useAuthStore = create( persist( (set, get) => { - // Axios interceptor for handling 401/403 - axios.interceptors.response.use( - (response) => response, - (error) => { - if (error.response && (error.response.status === 401 || error.response.status === 403)) { - set({ user_id: null, token: null, isLoading: false }); - delete axios.defaults.headers.common["Authorization"]; - // Redirect to login using window.location for global interceptor - if (window.location.pathname !== '/login') { - window.location.href = '/login'; - } - } - return Promise.reject(error); - } - ); + registerAuthInterceptor(set); return { user_id: null,