From f796ff9a969e8f2486949c84f65c3aee6813454d Mon Sep 17 00:00:00 2001 From: AzenKain Date: Wed, 13 May 2026 17:02:15 +0700 Subject: [PATCH] feat: implement core admin dashboard infrastructure including service layers, UI components, and submission management pages --- .../(admin)/(others-pages)/account/page.tsx | 6 +- src/app/(admin)/(others-pages)/blank/page.tsx | 2 +- .../(others-pages)/submissions/page.tsx | 1 - src/app/(admin)/(ui-elements)/badge/page.tsx | 12 +-- src/app/globals.css | 2 +- src/components/calendar/Calendar.tsx | 19 ++-- src/components/common/ComponentCard.tsx | 4 +- src/components/ecommerce/DailyNewChart.tsx | 2 +- src/components/ecommerce/DemographicCard.tsx | 2 +- .../ecommerce/DetailedStatsTable.tsx | 2 +- src/components/ecommerce/EcommerceMetrics.tsx | 8 +- .../ecommerce/MonthlySalesChart.tsx | 2 +- src/components/ecommerce/MonthlyTarget.tsx | 2 +- src/components/ecommerce/RecentOrders.tsx | 6 +- src/components/ecommerce/StatisticsChart.tsx | 2 +- src/components/tables/ApplicationTable.tsx | 2 +- src/components/tables/BasicTableOne.tsx | 10 +- src/components/tables/MediaTable.tsx | 2 +- src/config/axiosInstance.ts | 37 ------- src/config/config.ts | 99 ++++++++++++------- src/interface/common.ts | 15 ++- src/interface/historian.ts | 2 +- src/interface/project.ts | 62 ++++++++---- src/layout/SidebarWidget.tsx | 2 +- src/service/adminService.ts | 4 +- src/service/auth.ts | 10 +- src/service/historianService.ts | 4 +- src/service/mediaService.ts | 4 +- src/service/projectService.ts | 2 +- src/service/statisticsService.ts | 2 +- src/service/submisisonService.ts | 2 +- src/service/userService.ts | 2 +- 32 files changed, 171 insertions(+), 162 deletions(-) delete mode 100644 src/config/axiosInstance.ts diff --git a/src/app/(admin)/(others-pages)/account/page.tsx b/src/app/(admin)/(others-pages)/account/page.tsx index 8b2c08a..90f0b7f 100644 --- a/src/app/(admin)/(others-pages)/account/page.tsx +++ b/src/app/(admin)/(others-pages)/account/page.tsx @@ -13,7 +13,7 @@ export default function Profile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const dispatch = useDispatch(); - + useEffect(() => { const fetchUser = async () => { try { @@ -28,10 +28,10 @@ export default function Profile() { }; fetchUser(); }, []); - + return (
-
+

Profile

diff --git a/src/app/(admin)/(others-pages)/blank/page.tsx b/src/app/(admin)/(others-pages)/blank/page.tsx index 42b9add..67fba41 100644 --- a/src/app/(admin)/(others-pages)/blank/page.tsx +++ b/src/app/(admin)/(others-pages)/blank/page.tsx @@ -11,7 +11,7 @@ export default function BlankPage() { return (
-
+

Card Title Here diff --git a/src/app/(admin)/(others-pages)/submissions/page.tsx b/src/app/(admin)/(others-pages)/submissions/page.tsx index 0804b4c..c35b240 100644 --- a/src/app/(admin)/(others-pages)/submissions/page.tsx +++ b/src/app/(admin)/(others-pages)/submissions/page.tsx @@ -240,7 +240,6 @@ export default function Page() { } else { toast.error("Lỗi hệ thống khi cập nhật"); } - console.error(error); } finally { setIsSubmitting(false); } diff --git a/src/app/(admin)/(ui-elements)/badge/page.tsx b/src/app/(admin)/(ui-elements)/badge/page.tsx index 7dde8dc..95ab747 100644 --- a/src/app/(admin)/(ui-elements)/badge/page.tsx +++ b/src/app/(admin)/(ui-elements)/badge/page.tsx @@ -16,7 +16,7 @@ export default function BadgePage() {
-
+

With Light Background @@ -50,7 +50,7 @@ export default function BadgePage() {

-
+

With Solid Background @@ -84,7 +84,7 @@ export default function BadgePage() {

-
+

Light Background with Left Icon @@ -117,7 +117,7 @@ export default function BadgePage() {

-
+

Solid Background with Left Icon @@ -150,7 +150,7 @@ export default function BadgePage() {

-
+

Light Background with Right Icon @@ -183,7 +183,7 @@ export default function BadgePage() {

-
+

Solid Background with Right Icon diff --git a/src/app/globals.css b/src/app/globals.css index b801797..e837c7b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -554,7 +554,7 @@ span.flatpickr-weekday, @apply p-2; } .fc .fc-daygrid-day.fc-day-today .fc-scrollgrid-sync-inner { - @apply rounded-sm bg-gray-100 dark:bg-white/[0.03]; + @apply rounded-sm bg-gray-100 dark:bg-white/3; } .fc .fc-daygrid-day-number { @apply !p-3 text-sm font-medium text-gray-700 dark:text-gray-400; diff --git a/src/components/calendar/Calendar.tsx b/src/components/calendar/Calendar.tsx index 75657fb..64d509f 100644 --- a/src/components/calendar/Calendar.tsx +++ b/src/components/calendar/Calendar.tsx @@ -87,12 +87,12 @@ const Calendar: React.FC = () => { prevEvents.map((event) => event.id === selectedEvent.id ? { - ...event, - title: eventTitle, - start: eventStartDate, - end: eventEndDate, - extendedProps: { calendar: eventLevel }, - } + ...event, + title: eventTitle, + start: eventStartDate, + end: eventEndDate, + extendedProps: { calendar: eventLevel }, + } : event ) ); @@ -121,7 +121,7 @@ const Calendar: React.FC = () => { }; return ( -
+
{ /> diff --git a/src/components/common/ComponentCard.tsx b/src/components/common/ComponentCard.tsx index 42b03d0..ad66740 100644 --- a/src/components/common/ComponentCard.tsx +++ b/src/components/common/ComponentCard.tsx @@ -17,7 +17,7 @@ const ComponentCard: React.FC = ({ }) => { return (
{/* Card Header */}
@@ -31,7 +31,7 @@ const ComponentCard: React.FC = ({ )} {headerAction &&
{headerAction}
}
- + {/* Card Body */}
{children}
diff --git a/src/components/ecommerce/DailyNewChart.tsx b/src/components/ecommerce/DailyNewChart.tsx index 84d3b15..f00d123 100644 --- a/src/components/ecommerce/DailyNewChart.tsx +++ b/src/components/ecommerce/DailyNewChart.tsx @@ -133,7 +133,7 @@ export default function MonthlyNewChart() { }; return ( -
+

Lượng tạo mới hàng tháng diff --git a/src/components/ecommerce/DemographicCard.tsx b/src/components/ecommerce/DemographicCard.tsx index f2b552b..af373ad 100644 --- a/src/components/ecommerce/DemographicCard.tsx +++ b/src/components/ecommerce/DemographicCard.tsx @@ -19,7 +19,7 @@ export default function DemographicCard() { } return ( -
+

diff --git a/src/components/ecommerce/DetailedStatsTable.tsx b/src/components/ecommerce/DetailedStatsTable.tsx index 4581718..c7e8e26 100644 --- a/src/components/ecommerce/DetailedStatsTable.tsx +++ b/src/components/ecommerce/DetailedStatsTable.tsx @@ -52,7 +52,7 @@ export default function DetailedStatsTable() { }; return ( -
+

Bảng Số Liệu Chi Tiết (Hôm nay vs Hôm qua)

diff --git a/src/components/ecommerce/EcommerceMetrics.tsx b/src/components/ecommerce/EcommerceMetrics.tsx index 0b0b212..df11d83 100644 --- a/src/components/ecommerce/EcommerceMetrics.tsx +++ b/src/components/ecommerce/EcommerceMetrics.tsx @@ -47,8 +47,7 @@ export const EcommerceMetrics = () => { return (
- {/* */} -
+
@@ -68,10 +67,8 @@ export const EcommerceMetrics = () => {
- {/* */} - {/* */} -
+
@@ -91,7 +88,6 @@ export const EcommerceMetrics = () => {
- {/* */}
); }; diff --git a/src/components/ecommerce/MonthlySalesChart.tsx b/src/components/ecommerce/MonthlySalesChart.tsx index ed387ce..447faba 100644 --- a/src/components/ecommerce/MonthlySalesChart.tsx +++ b/src/components/ecommerce/MonthlySalesChart.tsx @@ -108,7 +108,7 @@ export default function MonthlySalesChart() { } return ( -
+

Monthly Sales diff --git a/src/components/ecommerce/MonthlyTarget.tsx b/src/components/ecommerce/MonthlyTarget.tsx index d4f66db..0a8854f 100644 --- a/src/components/ecommerce/MonthlyTarget.tsx +++ b/src/components/ecommerce/MonthlyTarget.tsx @@ -73,7 +73,7 @@ export default function MonthlyTarget() { } return ( -
+
diff --git a/src/components/ecommerce/RecentOrders.tsx b/src/components/ecommerce/RecentOrders.tsx index d06a073..513fed4 100644 --- a/src/components/ecommerce/RecentOrders.tsx +++ b/src/components/ecommerce/RecentOrders.tsx @@ -71,7 +71,7 @@ const tableData: Product[] = [ export default function RecentOrders() { return ( -
+

@@ -194,8 +194,8 @@ export default function RecentOrders() { product.status === "Delivered" ? "success" : product.status === "Pending" - ? "warning" - : "error" + ? "warning" + : "error" } > {product.status} diff --git a/src/components/ecommerce/StatisticsChart.tsx b/src/components/ecommerce/StatisticsChart.tsx index ff29ac0..cb7a25d 100644 --- a/src/components/ecommerce/StatisticsChart.tsx +++ b/src/components/ecommerce/StatisticsChart.tsx @@ -122,7 +122,7 @@ export default function StatisticsChart() { }; return ( -
+

diff --git a/src/components/tables/ApplicationTable.tsx b/src/components/tables/ApplicationTable.tsx index 3740a7d..3afb269 100644 --- a/src/components/tables/ApplicationTable.tsx +++ b/src/components/tables/ApplicationTable.tsx @@ -147,7 +147,7 @@ export default function ApplicationTable({ }; return ( -
+
diff --git a/src/components/tables/BasicTableOne.tsx b/src/components/tables/BasicTableOne.tsx index ec7b9e0..c786d42 100644 --- a/src/components/tables/BasicTableOne.tsx +++ b/src/components/tables/BasicTableOne.tsx @@ -84,7 +84,7 @@ export default function BasicTableOne({ }; return ( -
+
@@ -236,10 +236,10 @@ export default function BasicTableOne({ {role.name} )) || ( - - No Role - - )} + + No Role + + )} diff --git a/src/components/tables/MediaTable.tsx b/src/components/tables/MediaTable.tsx index b6fe8f0..fd22d4a 100644 --- a/src/components/tables/MediaTable.tsx +++ b/src/components/tables/MediaTable.tsx @@ -121,7 +121,7 @@ export default function MediaTable({ const isAllSelected = data.length > 0 && selectedIds.length === data.length; return ( -
+
diff --git a/src/config/axiosInstance.ts b/src/config/axiosInstance.ts deleted file mode 100644 index 4ae93e5..0000000 --- a/src/config/axiosInstance.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from "axios"; -import { API } from "../../api"; - -const axiosInstance = axios.create({ - baseURL: "/", - withCredentials: true, - headers: { - "Content-Type": "application/json", - }, -}); - -axiosInstance.interceptors.response.use( - (response) => { - if (response.data && response.data.status === false) { - return handleRefreshToken(response); - } - return response; - }, - async (error) => { - return Promise.reject(error); - } -); - -async function handleRefreshToken(originalResponse: any) { - try { - const refreshRes = await axios.get(API.Auth.REFRESH, { withCredentials: true }); - - if (refreshRes.data && refreshRes.data.status !== false) { - return axiosInstance(originalResponse.config); - } - } catch (err) { - console.error("Refresh token failed", err); - } - return originalResponse; -} - -export default axiosInstance; \ No newline at end of file diff --git a/src/config/config.ts b/src/config/config.ts index a961321..43f6edc 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,65 +1,88 @@ -import axios from "axios" -import { API_URL_ROOT } from "../../api" +import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"; -const baseURL = API_URL_ROOT || "https://history-api.kain.id.vn" +export const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3344"; -const api = axios.create({ +export const api = axios.create({ baseURL, - withCredentials: true -}) + withCredentials: true, +}); -let isRefreshing = false -let queue: any[] = [] - -const processQueue = (error?: any) => { - queue.forEach((p) => { - if (error) p.reject(error) - else p.resolve() - }) - queue = [] +interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { + _retry?: boolean; } +interface QueueItem { + resolve: () => void; + reject: (error: unknown) => void; +} + +let isRefreshing = false; +let queue: QueueItem[] = []; + +const processQueue = (error: unknown = null) => { + queue.forEach((p) => { + if (error) p.reject(error); + else p.resolve(); + }); + queue = []; +}; + +const skipRefreshUrls = [ + "/auth/signin", + "/auth/signup", + "/auth/logout", + "/auth/refresh", + "/auth/forgot-password", + "/auth/token/create", + "/auth/token/verify", +]; + api.interceptors.response.use( (res) => res, - async (err) => { - const originalRequest = err.config + async (err: AxiosError) => { + const originalRequest = err.config as CustomAxiosRequestConfig; - if (err.response?.status === 401 && !originalRequest._retry) { + const url = originalRequest?.url || ""; + + const shouldSkip = skipRefreshUrls?.some((path) => + url?.includes(path) + ); + + if ( + err.response?.status === 401 && + originalRequest && + !originalRequest._retry && + !shouldSkip + ) { if (isRefreshing) { return new Promise((resolve, reject) => { queue.push({ resolve: () => resolve(api(originalRequest)), - reject - }) - }) + reject: (queueErr) => reject(queueErr), + }); + }); } - originalRequest._retry = true - isRefreshing = true + originalRequest._retry = true; + isRefreshing = true; try { - await axios.post( - `${baseURL}/auth/refresh`, - {}, - { withCredentials: true } - ) + await axios.post(`${baseURL}/auth/refresh`, {}, { withCredentials: true }); - processQueue() + processQueue(null); - return api(originalRequest) + return api(originalRequest); } catch (refreshErr) { - processQueue(refreshErr) + processQueue(refreshErr); - window.location.href = "/signin" + window.location.href = "/auth/signin" - return Promise.reject(refreshErr) + return Promise.reject(refreshErr); } finally { - isRefreshing = false + isRefreshing = false; } } - return Promise.reject(err) + return Promise.reject(err); } -) - -export default api \ No newline at end of file +); diff --git a/src/interface/common.ts b/src/interface/common.ts index 8f3089d..6482a81 100644 --- a/src/interface/common.ts +++ b/src/interface/common.ts @@ -1,8 +1,15 @@ -export interface CommonResponse { +export interface ErrorResponse { + failed_field: string; + tag: string; + value: string; + message: string; +} + +export interface CommonResponse { status: boolean; message: string; data: T; - errors?: any; // Or a more specific error type + errors?: ErrorResponse[]; // Or a more specific error type } export interface PaginatedResponse { @@ -15,7 +22,7 @@ export interface PaginatedResponse { total_records: number; total_pages: number; }; - errors?: any; + errors?: ErrorResponse[]; } export interface CursorPaginatedResponse { @@ -25,5 +32,5 @@ export interface CursorPaginatedResponse { items: T[]; next_cursor_id?: string; }; - errors?: any; + errors?: ErrorResponse[]; } \ No newline at end of file diff --git a/src/interface/historian.ts b/src/interface/historian.ts index 2e927e2..d089457 100644 --- a/src/interface/historian.ts +++ b/src/interface/historian.ts @@ -19,7 +19,7 @@ export interface ApplicationDto { reviewed_at: string | null; created_at: string; updated_at?: string; - media: any[]; + media: MediaDto[]; user: { display_name?: string; avatar_url?: string; diff --git a/src/interface/project.ts b/src/interface/project.ts index 7f1d58c..2b7b068 100644 --- a/src/interface/project.ts +++ b/src/interface/project.ts @@ -1,22 +1,46 @@ +export interface CommitSimpleResponse { + id: string; + edit_summary: string; +} + +export interface MemberSimpleResponse { + user_id: string; + role: string; + display_name: string; + avatar_url: string; +} + +export interface SubmissionSimpleResponse { + id: string; + status: string; +} + +export interface UserSimpleResponse { + id: string; + email: string; + display_name: string; + avatar_url: string; +} + export interface Project { id: string; title: string; description: string; - project_status: "PRIVATE" | "PUBLIC" | "ARCHIVE"; - created_at: string; - updated_at: string; - is_deleted?: boolean; - user_id?: string; - user?: { - id: string; - email: string; - display_name: string; - avatar_url: string; - }; - commits?: any[]; - submission_ids?: any[]; - members?: ProjectMember[]; + latest_commit_id?: string | null; + project_status: "PRIVATE" | "PUBLIC" | "ARCHIVE" | string; + locked_by?: string | null; + is_deleted: boolean; + user_id: string; + created_at?: string | null; + updated_at?: string | null; + + user?: UserSimpleResponse | null; + + commits: CommitSimpleResponse[]; + submissions: SubmissionSimpleResponse[]; + members: MemberSimpleResponse[]; } + export interface ProjectsResponse { status: boolean; message: string; @@ -28,24 +52,22 @@ export interface ProjectsResponse { total_pages: number; }; } + export interface UpdateProjectPayload { title: string; description: string; status: "PRIVATE" | "PUBLIC" | "ARCHIVE"; } + export interface ChangeOwnerPayload { new_owner_id: string; } + export interface ProjectMemberPayload { user_id?: string; role: "EDITOR" | "VIEWER" | "ADMIN"; } -export interface ProjectMember { - user_id: string; - role: string; - display_name: string; - avatar_url: string; -} + export interface GetProjectsParams { page?: number; limit?: number; diff --git a/src/layout/SidebarWidget.tsx b/src/layout/SidebarWidget.tsx index 917908c..d788a39 100644 --- a/src/layout/SidebarWidget.tsx +++ b/src/layout/SidebarWidget.tsx @@ -4,7 +4,7 @@ export default function SidebarWidget() { return (

#1 Tailwind CSS Dashboard diff --git a/src/service/adminService.ts b/src/service/adminService.ts index 5508a5c..2f2ba68 100644 --- a/src/service/adminService.ts +++ b/src/service/adminService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { getUserDto } from "@/interface/admin"; @@ -38,6 +38,6 @@ export const apiUpdateApplicationStatus = async (id: string, payload: any) => { }; export const apiGetUserById = async (userId: string) => { - const response = await api.get(API.Admin.GET_USER_BY_ID(userId)); + const response = await api.get(API.Admin.GET_USER_BY_ID(userId)); return response?.data; }; \ No newline at end of file diff --git a/src/service/auth.ts b/src/service/auth.ts index e4798d5..caa0b87 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -1,11 +1,11 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; export const apiCreateOTP = async (email: string) => { const token_type = 2; - const response = await api.post(API.Auth.CREATEOTP, { - email, - token_type + const response = await api.post(API.Auth.CREATEOTP, { + email, + token_type }); return response.data; }; @@ -13,7 +13,7 @@ export const apiCreateOTP = async (email: string) => { export const apiVerifyOTP = async (email: string, token: string) => { const body = { email, token, token_type: 2 }; const response = await api.post(API.Auth.VERIFYOTP, body); - return response.data; + return response.data; }; export const apiSignUp = async (payload: any) => { diff --git a/src/service/historianService.ts b/src/service/historianService.ts index 49e50bb..3847df0 100644 --- a/src/service/historianService.ts +++ b/src/service/historianService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; export const createHistorianCV = async (payload: any) => { @@ -6,7 +6,7 @@ export const createHistorianCV = async (payload: any) => { return response?.data; }; -export const apiGetUserApplications = async (payload :any) => { +export const apiGetUserApplications = async (payload: any) => { const response = await api.get(API.Historian.APPLICATION, { params: payload }); return response?.data; }; diff --git a/src/service/mediaService.ts b/src/service/mediaService.ts index 3a1358a..9655eff 100644 --- a/src/service/mediaService.ts +++ b/src/service/mediaService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { payloadPresignedMedia } from "@/interface/media"; import axios from "axios"; @@ -112,7 +112,7 @@ export const getMediaById = async (mediaId: number | string) => { export const deleteMedia = async (mediaIds: string[]) => { const response = await api.delete(API.Media.DELETE_MEDIA, { data: { - media_ids: mediaIds + media_ids: mediaIds } }); return response?.data; diff --git a/src/service/projectService.ts b/src/service/projectService.ts index eb299ec..5d0b769 100644 --- a/src/service/projectService.ts +++ b/src/service/projectService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { ProjectMemberPayload, ChangeOwnerPayload, CreateCommitPayload, GetProjectsParams, Project, RestoreCommitPayload, UpdateProjectPayload } from "@/interface/project"; import { CommonResponse, CursorPaginatedResponse, PaginatedResponse } from "@/interface/common"; diff --git a/src/service/statisticsService.ts b/src/service/statisticsService.ts index 48d4f1f..948a0ca 100644 --- a/src/service/statisticsService.ts +++ b/src/service/statisticsService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { GetStatisticsParams, StatisticResponse } from "@/interface/statistics"; import { CommonResponse } from "@/interface/common"; diff --git a/src/service/submisisonService.ts b/src/service/submisisonService.ts index 9c048fc..38c9572 100644 --- a/src/service/submisisonService.ts +++ b/src/service/submisisonService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { getSubmissionPayload, updateSubmissionPayload } from "@/interface/submission"; diff --git a/src/service/userService.ts b/src/service/userService.ts index 1134e4d..2c14222 100644 --- a/src/service/userService.ts +++ b/src/service/userService.ts @@ -1,4 +1,4 @@ -import api from "@/config/config"; +import { api } from "@/config/config"; import { API } from "../../api"; import { Profile } from "@/interface/user";