diff --git a/client/src/pages/Dashboard.jsx b/client/src/pages/Dashboard.jsx index 6a505f2..e287da2 100644 --- a/client/src/pages/Dashboard.jsx +++ b/client/src/pages/Dashboard.jsx @@ -1,43 +1,108 @@ -import React, { useMemo } from 'react'; -import { Link } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { motion } from 'framer-motion'; -import { FiActivity, FiArrowRight, FiCheckCircle, FiClock, FiCpu, FiTarget, FiTrendingUp, FiZap } from 'react-icons/fi'; -import { api } from '../lib/api'; -import { USE_MOCK, mockChallenges, mockDashboardSummary } from '../lib/mockData'; -import SkeletonCard from '../components/SkeletonCard'; -import EmptyState from '../components/EmptyState'; -import PageHeader from '../components/PageHeader'; +import React, { useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { motion } from "framer-motion"; +import { + FiActivity, + FiArrowRight, + FiCheckCircle, + FiClock, + FiCpu, + FiTarget, + FiTrendingUp, + FiZap, +} from "react-icons/fi"; +import { api } from "../lib/api"; +import { + USE_MOCK, + mockChallenges, + mockDashboardSummary, +} from "../lib/mockData"; +import SkeletonCard from "../components/SkeletonCard"; +import EmptyState from "../components/EmptyState"; +import PageHeader from "../components/PageHeader"; const MotionBlock = motion.div; +const buildChallengeQuery = ({ + page, + limit, + search, + difficulty, + category, + sortBy, + sortDir, +}) => { + const params = new URLSearchParams(); + params.set("page", page); + params.set("limit", limit); + params.set("sortBy", sortBy); + params.set("sortDir", sortDir); + if (search) params.set("search", search); + if (difficulty) params.set("difficulty", difficulty); + if (category) params.set("category", category); + return params.toString(); +}; + +const difficultyChips = [ + { value: "", label: "All" }, + { value: "Easy", label: "Easy" }, + { value: "Medium", label: "Medium" }, + { value: "Hard", label: "Hard" }, +]; + const Dashboard = () => { + const [filters, setFilters] = useState({ + page: 1, + limit: 4, + search: "", + difficulty: "", + category: "", + sortBy: "createdAt", + sortDir: "desc", + }); + + const handleFilterChange = (key, value) => { + setFilters((prev) => ({ ...prev, [key]: value, page: 1 })); + }; + const challengesQuery = useQuery({ - queryKey: ['dashboard-challenges'], + queryKey: ["dashboard-challenges", filters], queryFn: async () => { if (USE_MOCK) { - return mockChallenges.slice(0, 4); + let filtered = mockChallenges; + if (filters.search) { + filtered = filtered.filter(c => c.title.toLowerCase().includes(filters.search.toLowerCase()) || c.description.toLowerCase().includes(filters.search.toLowerCase())); + } + if (filters.difficulty) { + filtered = filtered.filter(c => c.difficulty === filters.difficulty); + } + if (filters.category) { + filtered = filtered.filter(c => c.category && c.category.toLowerCase().includes(filters.category.toLowerCase())); + } + return filtered.slice(0, filters.limit); } try { - const res = await api.get('/api/challenges?page=1&limit=4&sortBy=createdAt&sortDir=desc'); + const qs = buildChallengeQuery(filters); + const res = await api.get(`/api/challenges?${qs}`); const data = res.data.data || []; - return data.length > 0 ? data : mockChallenges.slice(0, 4); + return data.length > 0 ? data : mockChallenges.slice(0, filters.limit); } catch { - return mockChallenges.slice(0, 4); + return mockChallenges.slice(0, filters.limit); } }, }); const summaryQuery = useQuery({ - queryKey: ['dashboard-summary'], + queryKey: ["dashboard-summary"], queryFn: async () => { if (USE_MOCK) { return mockDashboardSummary; } try { - const res = await api.get('/api/dashboard/summary'); + const res = await api.get("/api/dashboard/summary"); return res.data.data || mockDashboardSummary; } catch { return mockDashboardSummary; @@ -50,33 +115,35 @@ const Dashboard = () => { ? summaryQuery.data.recentActivity : mockDashboardSummary.recentActivity; const solvedRate = summaryQuery.data?.totalChallenges - ? Math.round((summaryQuery.data.solved / summaryQuery.data.totalChallenges) * 100) + ? Math.round( + (summaryQuery.data.solved / summaryQuery.data.totalChallenges) * 100, + ) : 0; const stats = [ { - label: 'Total Challenges', - value: summaryQuery.data?.totalChallenges ?? '-', + label: "Total Challenges", + value: summaryQuery.data?.totalChallenges ?? "-", icon: FiTarget, - valueClass: 'text-primary', + valueClass: "text-primary", }, { - label: 'Solved', - value: summaryQuery.data?.solved ?? '-', + label: "Solved", + value: summaryQuery.data?.solved ?? "-", icon: FiCheckCircle, - valueClass: 'text-green-500', + valueClass: "text-green-500", }, { - label: 'Pending Reviews', - value: summaryQuery.data?.pending ?? '-', + label: "Pending Reviews", + value: summaryQuery.data?.pending ?? "-", icon: FiClock, - valueClass: 'text-yellow-500', + valueClass: "text-yellow-500", }, { - label: 'Solved Rate', + label: "Solved Rate", value: `${solvedRate}%`, icon: FiTrendingUp, - valueClass: 'text-accent', + valueClass: "text-accent", }, ]; @@ -110,14 +177,19 @@ const Dashboard = () => {

Mastering Dynamic
- Programming + + Programming +

- Push your limits with this week's elite challenge. Solve complex optimizations and climb the global - leaderboards. + Push your limits with this week's elite challenge. Solve + complex optimizations and climb the global leaderboards.

- + Enter Arena
@@ -146,12 +218,18 @@ const Dashboard = () => {
-

{card.label}

+

+ {card.label} +

-

{card.value}

- ▲ 2% +

+ {card.value} +

+ + ▲ 2% +
); @@ -161,19 +239,78 @@ const Dashboard = () => {
-

Available Missions

- +

+ Available Missions +

+ View All
+ {/* Filter Bar */} +
+ handleFilterChange("search", e.target.value)} + /> + handleFilterChange("category", e.target.value)} + /> + +
+ + + +
+
+ +
+ {difficultyChips.map((chip) => ( + + ))} +
+ {challengesQuery.isLoading ? (
) : challenges.length === 0 ? ( - + ) : (
{challenges.map((challenge, index) => ( @@ -183,26 +320,33 @@ const Dashboard = () => { animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.03 }} > - +
{challenge.difficulty} - {challenge.points} XP + + {challenge.points} XP +

{challenge.title}

-

{challenge.description}

+

+ {challenge.description} +

@@ -234,17 +378,19 @@ const Dashboard = () => { className="block rounded-xl border border-glass-border bg-white/[0.01] p-4 transition-all hover:border-accent hover:bg-accent/5" >

- {submission.challengeId?.title || 'Unknown Challenge'} + {submission.challengeId?.title || "Unknown Challenge"}

-

{new Date(submission.submittedAt).toLocaleDateString()}

+

+ {new Date(submission.submittedAt).toLocaleDateString()} +

{submission.status} @@ -253,7 +399,9 @@ const Dashboard = () => { )) ) : ( -

No recent submissions yet. Start with an easy challenge.

+

+ No recent submissions yet. Start with an easy challenge. +

)}
diff --git a/client/src/pages/Missions.jsx b/client/src/pages/Missions.jsx index 44dd2da..500627c 100644 --- a/client/src/pages/Missions.jsx +++ b/client/src/pages/Missions.jsx @@ -61,18 +61,44 @@ const Missions = () => { const challengesQuery = useQuery({ queryKey, queryFn: async () => { + const getFilteredMockData = () => { + let filtered = mockChallenges; + if (filters.search) { + const searchLower = filters.search.toLowerCase(); + filtered = filtered.filter(c => c.title.toLowerCase().includes(searchLower) || c.description.toLowerCase().includes(searchLower)); + } + if (filters.difficulty) { + filtered = filtered.filter(c => c.difficulty === filters.difficulty); + } + if (filters.category) { + const categoryLower = filters.category.toLowerCase(); + filtered = filtered.filter(c => c.category && c.category.toLowerCase().includes(categoryLower)); + } + return filtered; + }; + try { const qs = buildChallengeQuery(filters); const res = await api.get(`/api/challenges?${qs}`); const data = res.data.data || []; + + if (data.length > 0) { + return { + data, + meta: res.data.meta || { page: 1, totalPages: 1, total: data.length }, + }; + } + + const filteredMock = getFilteredMockData(); return { - data: data.length > 0 ? data : mockChallenges, - meta: res.data.meta || { page: 1, totalPages: 1, total: mockChallenges.length }, + data: filteredMock, + meta: { page: 1, totalPages: Math.ceil(filteredMock.length / filters.limit) || 1, total: filteredMock.length }, }; } catch { + const filteredMock = getFilteredMockData(); return { - data: mockChallenges, - meta: { page: 1, totalPages: 1, total: mockChallenges.length }, + data: filteredMock, + meta: { page: 1, totalPages: Math.ceil(filteredMock.length / filters.limit) || 1, total: filteredMock.length }, }; } }, @@ -80,14 +106,14 @@ const Missions = () => { const challenges = challengesQuery.data?.data?.length ? challengesQuery.data.data : mockChallenges; const meta = challengesQuery.data?.meta || { page: 1, totalPages: 1, total: challenges.length }; - + const groupedChallenges = useMemo(() => { if (filters.grouping === 'none') return { "All Missions": challenges }; - + return challenges.reduce((acc, ch) => { const date = new Date(ch.createdAt || FALLBACK_CREATED_AT); let key = ""; - + if (filters.grouping === 'weekly') { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); const pastDaysOfYear = (date - firstDayOfYear) / 86400000; @@ -96,7 +122,7 @@ const Missions = () => { } else if (filters.grouping === 'monthly') { key = date.toLocaleString('default', { month: 'long', year: 'numeric' }); } - + if (!acc[key]) acc[key] = []; acc[key].push(ch); return acc; @@ -173,7 +199,7 @@ const Missions = () => {
{/* Left Side: Difficulty Chips */} -
+
{difficultyChips.map((chip) => (