diff --git a/client/portal/src/pages/admin/_shared/AppSidebar.tsx b/client/portal/src/pages/admin/_shared/AppSidebar.tsx
index 15834b0c..f1d57621 100644
--- a/client/portal/src/pages/admin/_shared/AppSidebar.tsx
+++ b/client/portal/src/pages/admin/_shared/AppSidebar.tsx
@@ -11,6 +11,7 @@ import {
ScanLine,
Settings,
Star,
+ Trophy,
UserCheck,
Users,
} from "lucide-react";
@@ -66,6 +67,11 @@ const eventNav = [
url: "/admin/faq",
icon: MessageSquare,
},
+ {
+ name: "Tracks",
+ url: "/admin/tracks",
+ icon: Trophy,
+ },
];
const superAdminNav = [
diff --git a/client/portal/src/pages/admin/index.ts b/client/portal/src/pages/admin/index.ts
index 06c8cf92..e23c41a3 100644
--- a/client/portal/src/pages/admin/index.ts
+++ b/client/portal/src/pages/admin/index.ts
@@ -4,3 +4,4 @@ export { default as ReviewsPage } from "./reviews/ReviewsPage";
export { default as ScansPage } from "./scans/ScansPage";
export { default as SchedulePage } from "./schedule/SchedulePage";
export { default as SponsorsPage } from "./sponsors/SponsorsPage";
+export { default as TracksPage } from "./tracks/TracksPage";
diff --git a/client/portal/src/pages/admin/tracks/TracksPage.tsx b/client/portal/src/pages/admin/tracks/TracksPage.tsx
new file mode 100644
index 00000000..b805f40c
--- /dev/null
+++ b/client/portal/src/pages/admin/tracks/TracksPage.tsx
@@ -0,0 +1,58 @@
+import { useEffect } from "react";
+
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+import { TracksTable } from "./components/TracksTable";
+import { useTracksStore } from "./store";
+
+export default function TracksPage() {
+ const {
+ tracks,
+ canEdit,
+ loading,
+ saving,
+ fetch: loadTracks,
+ createTrack,
+ updateTrack,
+ deleteTrack,
+ uploadLogo,
+ } = useTracksStore();
+
+ useEffect(() => {
+ const controller = new AbortController();
+ loadTracks(controller.signal);
+ return () => controller.abort();
+ }, [loadTracks]);
+
+ if (loading && tracks.length === 0) {
+ return (
+
+
+
+
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/client/portal/src/pages/admin/tracks/api.ts b/client/portal/src/pages/admin/tracks/api.ts
new file mode 100644
index 00000000..083b0e78
--- /dev/null
+++ b/client/portal/src/pages/admin/tracks/api.ts
@@ -0,0 +1,61 @@
+import {
+ deleteRequest,
+ getRequest,
+ postRequest,
+ putRequest,
+} from "@/shared/lib/api";
+import type { ApiResponse } from "@/types";
+
+import type { Track, TrackListResponse, TrackPayload } from "./types";
+
+export async function fetchTracks(
+ signal?: AbortSignal,
+): Promise> {
+ return getRequest("/admin/tracks", "tracks", signal);
+}
+
+export async function fetchTrackEditPermission(
+ signal?: AbortSignal,
+): Promise> {
+ return getRequest<{ enabled: boolean }>(
+ "/admin/tracks/edit-permission",
+ "track edit permission",
+ signal,
+ );
+}
+
+export async function createTrack(
+ payload: TrackPayload,
+ signal?: AbortSignal,
+): Promise> {
+ return postRequest