Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 39 additions & 35 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="min-h-screen w-full bg-[#0A0F1A] text-slate-100">
Expand All @@ -56,13 +43,13 @@ const Dashboard = () => {
<div className="flex items-center gap-2 text-amber-300">
<Flame className="h-4 w-4" />
<span className="text-slate-200 md:inline hidden">
3 Day Streak
{streak} Day Streak
</span>
</div>
<div className="flex items-center gap-2 text-blue-300">
<Gem className="h-4 w-4" />
<span className="text-slate-200 md:inline hidden">
1.1K Points
{points} Points
</span>
</div>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-700/70 text-slate-100">
Expand All @@ -74,36 +61,53 @@ const Dashboard = () => {
</header>

<main className="mx-auto w-full max-w-6xl px-4 pt-20 pb-8 sm:max-w-6xl sm:px-6">
{isLoading && (
<div className="flex items-center justify-center py-8">
<div className="text-slate-400">Loading dashboard...</div>
</div>
)}

{error && (
<div className="flex items-center justify-center py-8">
<div className="text-red-400">Error: {error}</div>
</div>
)}

<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-6 text-xs text-slate-300">
<div className="flex items-center gap-2">
<Flame className="h-4 w-4 text-amber-300" />
<span>3 Day Streak</span>
<span>{streak} Day Streak</span>
</div>
<div className="flex items-center gap-2">
<Gem className="h-4 w-4 text-blue-300" />
<span>1.1K Points</span>
<span>{points} Points</span>
</div>
</div>
<DailyQuestCard
title="Daily Quest"
questionCount={5}
progressCurrent={2}
progressTotal={5}
questionCount={dailyQuestProgress.total}
progressCurrent={dailyQuestProgress.completed}
progressTotal={dailyQuestProgress.total}
/>
</div>

<section className="mt-10">
<h2 className="text-base font-semibold text-white">Categories</h2>
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-2">
{categories.length === 0 && !isLoading && (
<div className="col-span-full text-center text-slate-400 py-4">
No categories available
</div>
)}
{categories.map((category) => (
<CategoryCard
key={category.slug}
icon={category.icon}
key={category.id}
icon={category.icon ?? "🧩"}
name={category.name}
description={category.description}
userLevel={category.userLevel}
onClick={() => router.push(`/categories/${category.slug}`)}
description={category.description ?? ""}
userLevel="Level 1"
onClick={() => router.push(`/categories/${category.id}`)}
/>
))}
</div>
Expand Down
13 changes: 8 additions & 5 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -33,11 +34,13 @@ export default function RootLayout({
>
<StoreProvider>
<ToastProvider>
<CompletionFeatureProvider>
<ClientLayout>
{children}
</ClientLayout>
</CompletionFeatureProvider>
<DashboardFeatureProvider>
<CompletionFeatureProvider>
<ClientLayout>
{children}
</ClientLayout>
</CompletionFeatureProvider>
</DashboardFeatureProvider>
</ToastProvider>
</StoreProvider>
</body>
Expand Down
105 changes: 105 additions & 0 deletions frontend/features/dashboard/dashboard.api.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
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<T>(response: Response): Promise<T> {
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<DashboardStats> {
const headers: HeadersInit = {
"Content-Type": "application/json",
...getAuthHeaders(),
};

const response = await fetch(`${API_BASE_URL}/dashboard/stats`, {
method: "GET",
headers,
});

return handleResponse<DashboardStats>(response);
}

/**
* Fetch all available categories
* GET /categories
*/
export async function fetchCategories(): Promise<CategoriesResponse> {
const headers: HeadersInit = {
"Content-Type": "application/json",
...getAuthHeaders(),
};

const response = await fetch(`${API_BASE_URL}/categories`, {
method: "GET",
headers,
});

return handleResponse<CategoriesResponse>(response);
}
Loading