From b2f9c04569feacc747e689210a5d4fb0492b2303 Mon Sep 17 00:00:00 2001 From: kamaldeen Aliyu Date: Mon, 23 Feb 2026 12:24:02 +0100 Subject: [PATCH] Created the dashboard data fetching --- frontend/app/dashboard/page.tsx | 74 ++++---- frontend/app/layout.tsx | 13 +- frontend/features/dashboard/dashboard.api.ts | 105 +++++++++++ .../features/dashboard/dashboard.context.tsx | 166 ++++++++++++++++++ frontend/features/dashboard/index.ts | 27 +++ .../providers/DashboardFeatureProvider.tsx | 24 +++ 6 files changed, 369 insertions(+), 40 deletions(-) create mode 100644 frontend/features/dashboard/dashboard.api.ts create mode 100644 frontend/features/dashboard/dashboard.context.tsx create mode 100644 frontend/features/dashboard/index.ts create mode 100644 frontend/providers/DashboardFeatureProvider.tsx diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 695e5e4..157444d 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -5,33 +5,20 @@ import DailyQuestCard from "@/components/dashboard/DailyQuestCard"; import CategoryCard from "@/components/dashboard/CategoryCard"; import Image from "next/image"; import { Flame, Gem, User } from "lucide-react"; +import { useDashboard } from "@/features/dashboard"; const Dashboard = () => { const router = useRouter(); + const { data, isLoading, error } = useDashboard(); - const categories = [ - { - icon: "🧩", - name: "Puzzles", - description: "Pattern Recognition", - userLevel: "Level 5", - slug: "puzzles", - }, - { - icon: "💻", - name: "Coding", - description: "Algorithm and Data Structures", - userLevel: "Level 2", - slug: "coding", - }, - { - icon: "⛓️", - name: "Blockchain", - description: "Crypto & Defi Concepts", - userLevel: "Level 2", - slug: "blockchain", - }, - ]; + // Get stats from context or use defaults + const stats = data?.stats; + const streak = stats?.streak ?? 0; + const points = stats?.points ?? 0; + const dailyQuestProgress = stats?.dailyQuestProgress ?? { completed: 0, total: 5 }; + + // Get categories from context or use empty array + const categories = data?.categories ?? []; return (
@@ -56,13 +43,13 @@ const Dashboard = () => {
- 3 Day Streak + {streak} Day Streak
- 1.1K Points + {points} Points
@@ -74,36 +61,53 @@ const Dashboard = () => {
+ {isLoading && ( +
+
Loading dashboard...
+
+ )} + + {error && ( +
+
Error: {error}
+
+ )} +
- 3 Day Streak + {streak} Day Streak
- 1.1K Points + {points} Points

Categories

+ {categories.length === 0 && !isLoading && ( +
+ No categories available +
+ )} {categories.map((category) => ( router.push(`/categories/${category.slug}`)} + description={category.description ?? ""} + userLevel="Level 1" + onClick={() => router.push(`/categories/${category.id}`)} /> ))}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index a780c81..b44ae2c 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -4,6 +4,7 @@ import { ToastProvider } from "@/components/ui/ToastProvider"; import StoreProvider from "@/providers/storeProvider"; import ClientLayout from "@/components/ClientLayout"; import CompletionFeatureProvider from "@/providers/CompletionFeatureProvider"; +import DashboardFeatureProvider from "@/providers/DashboardFeatureProvider"; const poppins = Poppins({ subsets: ["latin"], @@ -33,11 +34,13 @@ export default function RootLayout({ > - - - {children} - - + + + + {children} + + + diff --git a/frontend/features/dashboard/dashboard.api.ts b/frontend/features/dashboard/dashboard.api.ts new file mode 100644 index 0000000..5cab649 --- /dev/null +++ b/frontend/features/dashboard/dashboard.api.ts @@ -0,0 +1,105 @@ +/** + * Dashboard API Module + * + * Handles all API calls related to dashboard data fetching. + * Includes endpoints for stats and categories. + */ + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000"; + +// Types for API responses +export interface DashboardStats { + streak: number; + points: number; + dailyQuestProgress: { + completed: number; + total: number; + }; +} + +export interface Category { + id: string; + name: string; + description?: string; + icon?: string; + isActive: boolean; + createdAt: string; +} + +export interface CategoriesResponse { + success: boolean; + data: Category[]; + count: number; + message?: string; + error?: string; +} + +// Helper function to get auth headers +function getAuthHeaders(): Record { + if (typeof window === "undefined") { + return {}; + } + + const token = window.localStorage.getItem("accessToken"); + + if (!token) { + return {}; + } + + return { + Authorization: `Bearer ${token}`, + }; +} + +// Helper function to handle API responses +async function handleResponse(response: Response): Promise { + const contentType = response.headers.get("Content-Type"); + const isJson = contentType && contentType.includes("application/json"); + + const data = isJson ? await response.json() : null; + + if (!response.ok) { + const message = + (data && (data.message as string | undefined)) || + `Request failed with status ${response.status}`; + throw new Error(message); + } + + return data as T; +} + +/** + * Fetch dashboard stats including streak, points, and daily quest progress + * GET /dashboard/stats + */ +export async function fetchDashboardStats(): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + ...getAuthHeaders(), + }; + + const response = await fetch(`${API_BASE_URL}/dashboard/stats`, { + method: "GET", + headers, + }); + + return handleResponse(response); +} + +/** + * Fetch all available categories + * GET /categories + */ +export async function fetchCategories(): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + ...getAuthHeaders(), + }; + + const response = await fetch(`${API_BASE_URL}/categories`, { + method: "GET", + headers, + }); + + return handleResponse(response); +} diff --git a/frontend/features/dashboard/dashboard.context.tsx b/frontend/features/dashboard/dashboard.context.tsx new file mode 100644 index 0000000..f7e5218 --- /dev/null +++ b/frontend/features/dashboard/dashboard.context.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { + createContext, + useContext, + useState, + useCallback, + useEffect, + useRef, +} from "react"; +import { + fetchDashboardStats, + fetchCategories, + DashboardStats, + Category, +} from "./dashboard.api"; + +/** + * Dashboard Context + * + * Provides dashboard data (stats and categories) to all child components. + * Handles loading states, error states, and prevents duplicate API calls. + */ + +// Dashboard data shape exposed to UI +export interface DashboardData { + stats: { + streak: number; + points: number; + dailyQuestProgress: { + completed: number; + total: number; + }; + }; + categories: Category[]; +} + +// Context type definition +interface DashboardContextType { + data: DashboardData | null; + isLoading: boolean; + error: string | null; + refreshDashboard: () => Promise; +} + +// Create context with undefined default +const DashboardContext = createContext( + undefined +); + +// Default stats when API returns no data +const defaultStats: DashboardData["stats"] = { + streak: 0, + points: 0, + dailyQuestProgress: { + completed: 0, + total: 5, + }, +}; + +// Props for the provider +interface DashboardProviderProps { + children: React.ReactNode; + autoFetch?: boolean; +} + +/** + * Dashboard Provider Component + * + * Wraps children with dashboard data context. + * Fetches stats and categories on mount (if autoFetch is true). + */ +export const DashboardProvider: React.FC = ({ + children, + autoFetch = true, +}) => { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Use ref to track if data has been fetched to prevent duplicate calls + const hasFetchedRef = useRef(false); + + /** + * Refresh dashboard data + * Fetches both stats and categories in parallel + */ + const refreshDashboard = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + // Fetch stats and categories in parallel + const [statsResponse, categoriesResponse] = await Promise.all([ + fetchDashboardStats().catch(() => null), + fetchCategories().catch(() => null), + ]); + + // Process stats response + const stats: DashboardData["stats"] = statsResponse ?? defaultStats; + + // Process categories response + const categories: Category[] = + categoriesResponse?.success && Array.isArray(categoriesResponse.data) + ? categoriesResponse.data + : []; + + setData({ + stats, + categories, + }); + } catch (err: any) { + setError(err.message || "Failed to fetch dashboard data"); + // Set default data on error + setData({ + stats: defaultStats, + categories: [], + }); + } finally { + setIsLoading(false); + } + }, []); + + // Auto-fetch data on mount if enabled + useEffect(() => { + if (autoFetch && !hasFetchedRef.current) { + hasFetchedRef.current = true; + refreshDashboard(); + } + }, [autoFetch, refreshDashboard]); + + const value: DashboardContextType = { + data, + isLoading, + error, + refreshDashboard, + }; + + return ( + + {children} + + ); +}; + +/** + * useDashboard Hook + * + * Custom hook to consume dashboard context. + * Must be used within a DashboardProvider. + * + * @example + * const { data, isLoading, error, refreshDashboard } = useDashboard(); + */ +export function useDashboard(): DashboardContextType { + const context = useContext(DashboardContext); + + if (context === undefined) { + throw new Error("useDashboard must be used within a DashboardProvider"); + } + + return context; +} + +// Re-export types for convenience +export type { DashboardStats, Category }; diff --git a/frontend/features/dashboard/index.ts b/frontend/features/dashboard/index.ts new file mode 100644 index 0000000..9c28114 --- /dev/null +++ b/frontend/features/dashboard/index.ts @@ -0,0 +1,27 @@ +/** + * Dashboard Feature Module + * + * Centralized exports for dashboard-related functionality. + * Use this barrel file to import dashboard components, hooks, and types. + * + * @example + * import { DashboardProvider, useDashboard } from '@/features/dashboard'; + */ + +// Export API functions and types +export { + fetchDashboardStats, + fetchCategories, + type DashboardStats, + type Category, + type CategoriesResponse, +} from "./dashboard.api"; + +// Export context, provider, and hook +export { + DashboardProvider, + useDashboard, + type DashboardData, + type DashboardStats as DashboardStatsType, + type Category as CategoryType, +} from "./dashboard.context"; diff --git a/frontend/providers/DashboardFeatureProvider.tsx b/frontend/providers/DashboardFeatureProvider.tsx new file mode 100644 index 0000000..d983c81 --- /dev/null +++ b/frontend/providers/DashboardFeatureProvider.tsx @@ -0,0 +1,24 @@ +/** + * Dashboard Feature Provider + * + * Wrapper component that provides dashboard data context to the application. + * This separates the provider setup from the layout for cleaner code organization. + */ + +import { DashboardProvider } from "../features/dashboard"; + +interface DashboardFeatureProviderProps { + children: React.ReactNode; +} + +/** + * DashboardFeatureProvider + * + * Wraps children with DashboardProvider to enable dashboard data fetching + * and state management throughout the application. + */ +export default function DashboardFeatureProvider({ + children, +}: DashboardFeatureProviderProps) { + return {children}; +}