feat(backoffice): FE integration (data fetch) for dashboard, teams, submission, and user management page (#70)
* feat(backoffice): create a boilerplate page for Hackathon dashboard - Create an empty page for Hackathon dashboard - Comment out and hide the existing backoffice sidebar * feat(backoffice): Create a nested/dropdown sidebar list - Create a dropdown sidebar list - Show back the old navigation and group them - Make the sidebar responsive for mobile view * test datatable with mock data * base UI for Hackathon backoffice TODO: - Organize table schema for users, teams, and submissions management - Create API Contract for additional back-end endpoint * feat(hackathon): draft data table column & API Contract * update endpoint * feat(backoffice): update page hackathon user management - update data table component - update filtering & pagination - add modal display to edit and add user - hide notification icon in backoffice wrapper * feat(backoffice): add API contract for hackathon users * feat(backoffice): little adjustment in hackathon users management UI and API contract * feat(backoffice): update hackathon team management page - add modal for manage team, add new team, and view project submission - reorganize the table column and data table * feat(backoffice): add searchable city filter - add component for city filter - apply to user management and team management pages * feat(backoffice): improve hackathon team modal UI and add API contract - Add feature to select city in team detail modal using CityFilterSelect component - Add feature to change team logo and banner - Add API contract documentation for hackathon teams in backoffice * feat(backoffice): authentication middleware, error pages, and 404 page * feat(backoffice): hackathon dashboard integration * feat(backoffice): users page integration - Get users data from API - Set up server-side pagination and match URL params - Hide filter that doesn't exist in back-end * feat(backoffice): teams page integration - Get teams data from API - Hide filter that doesn't exists in back-end - Simplify modal according to the back-end * feat(backoffice): submission page integration - Fetch submissions data from API - Move submission modal to hackathon-submissions page
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { api } from '../index';
|
||||
import type {
|
||||
TAdminUsersResponse,
|
||||
TAdminTeamsResponse,
|
||||
TAdminSubmissionsResponse,
|
||||
} from '../../types/admin';
|
||||
|
||||
const ADMIN_BASE_URL = '/admin';
|
||||
|
||||
// Admin Users
|
||||
export const getAdminUsers = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
is_admin?: boolean;
|
||||
}) => {
|
||||
const response = await api.get<TAdminUsersResponse>(
|
||||
`${ADMIN_BASE_URL}/users`,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
is_admin: params?.is_admin ?? false,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Admin Teams
|
||||
export const getAdminTeams = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminTeamsResponse>(
|
||||
`${ADMIN_BASE_URL}/teams`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Admin Submissions
|
||||
export const getAdminSubmissions = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
status?: string;
|
||||
}) => {
|
||||
const response = await api.get<TAdminSubmissionsResponse>(
|
||||
`${ADMIN_BASE_URL}/submissions`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '../hooks/auth';
|
||||
|
||||
// Backoffice Backend API Base URL
|
||||
// In development, use proxy; in production, use full URL
|
||||
const BACKOFFICE_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
|
||||
|
||||
// Create axios instance for backoffice backend
|
||||
export const backofficeApi = axios.create({
|
||||
baseURL: BACKOFFICE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token interceptor
|
||||
backofficeApi.interceptors.request.use(
|
||||
(config) => {
|
||||
const { session } = useAuthStore.getState();
|
||||
if (session?.token?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${session.token.access_token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
// Error handling interceptor
|
||||
backofficeApi.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// Handle 401 - clear session and redirect to login
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthPage =
|
||||
globalThis.window !== undefined &&
|
||||
globalThis.location.pathname.startsWith('/auth');
|
||||
|
||||
if (!isAuthPage) {
|
||||
useAuthStore.getState().clearSession();
|
||||
if (globalThis.window !== undefined) {
|
||||
globalThis.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If backend sends a message, use it
|
||||
const backendMsg = error?.response?.data?.message;
|
||||
if (backendMsg && typeof backendMsg === 'string') {
|
||||
return Promise.reject(new Error(backendMsg));
|
||||
}
|
||||
|
||||
// Fallback error message
|
||||
return Promise.reject(new Error(error.message || 'An error occurred'));
|
||||
}
|
||||
);
|
||||
|
||||
// Response type
|
||||
export interface BackofficeApiResponse<T> {
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from './users';
|
||||
export * from './mentors';
|
||||
export * from './upload';
|
||||
export * from './hackathon';
|
||||
export * from './admin';
|
||||
|
||||
// Common API response wrapper interface
|
||||
export interface ApiResponse<T> {
|
||||
@@ -20,7 +21,9 @@ const getSessionTokenFromCookies = () => {
|
||||
if (typeof document === 'undefined') return null;
|
||||
|
||||
const cookies = document.cookie.split(';');
|
||||
const tokenCookie = cookies.find(cookie => cookie.trim().startsWith(`${TOKEN_KEY}=`));
|
||||
const tokenCookie = cookies.find((cookie) =>
|
||||
cookie.trim().startsWith(`${TOKEN_KEY}=`)
|
||||
);
|
||||
|
||||
if (!tokenCookie) return null;
|
||||
|
||||
@@ -32,13 +35,17 @@ const getSessionTokenFromCookies = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const setSessionTokenToCookies = (tokenData: { token: { access_token: string; refresh_token: string } }) => {
|
||||
const setSessionTokenToCookies = (tokenData: {
|
||||
token: { access_token: string; refresh_token: string };
|
||||
}) => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7);
|
||||
|
||||
document.cookie = `${TOKEN_KEY}=${encodeURIComponent(JSON.stringify(tokenData))}; expires=${expires.toUTCString()}; path=/; secure; samesite=strict`;
|
||||
document.cookie = `${TOKEN_KEY}=${encodeURIComponent(
|
||||
JSON.stringify(tokenData)
|
||||
)}; expires=${expires.toUTCString()}; path=/; secure; samesite=strict`;
|
||||
};
|
||||
|
||||
const removeSessionTokenFromCookies = () => {
|
||||
|
||||
Reference in New Issue
Block a user