From 53f8189d40f338e100bec862ac18a86bde3a3646 Mon Sep 17 00:00:00 2001 From: Aleksandar Karastoyanov Date: Sun, 8 Mar 2026 09:42:17 +0200 Subject: [PATCH 1/3] Enhancement: Rework frontend UI: - dashboard cards reworked - users stats updated to dynamic - minor UI fixes Signed-off-by: Aleksandar Karastoyanov --- frontend/src/App.jsx | 20 +++++-- frontend/src/pages/Dashboard.jsx | 86 +++++++++++++++++++--------- frontend/src/services/authService.js | 17 +++++- 3 files changed, 90 insertions(+), 33 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 75b16ba..d90b125 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,7 +14,7 @@ import LoadingSpinner from "./components/Layout/LoadingSpinner"; /** * Redirect to login if the user is not authenticated. - * Shows nothing (spinner) while the auth state is being resolved on first load. + * Shows a spinner while the auth state is being resolved on first load. */ function ProtectedRoute({ children }) { const { user, loading } = useAuth(); @@ -23,14 +23,26 @@ function ProtectedRoute({ children }) { return children; } +/** + * Redirect to dashboard if the user is already authenticated. + * Prevents authenticated users from seeing the login/signup pages. + * Shows a spinner while auth state is resolving to avoid a flash of the form. + */ +function PublicRoute({ children }) { + const { user, loading } = useAuth(); + if (loading) return ; + if (user) return ; + return children; +} + export default function App() { return ( - {/* Public routes */} - } /> - } /> + {/* Public routes — redirect to dashboard if already authenticated */} + } /> + } /> {/* Protected routes */} } /> diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index de434ed..e324b84 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,6 +1,10 @@ +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import Navbar from "../components/Layout/Navbar"; import { useAuth } from "../context/AuthContext"; +import { getUserById } from "../services/usersService"; + +const TOTAL_QUESTS = 30; // 11 Python + 7 JS + 8 Java + 4 C# const languages = [ { @@ -24,7 +28,7 @@ const languages = [ path: "/quests/Java", image: "src/assets/img/achievements-icons/Java/java-5.png", buttonClass: "java_button", - description: "Enterprise & Android", + description: "Enterprise, Android & more", quests: 8, }, { @@ -60,7 +64,7 @@ const sections = [ function SectionDivider({ title }) { return ( -
+

{title}

@@ -85,6 +89,26 @@ export default function Dashboard() { const { user } = useAuth(); const displayName = user?.username || "Adventurer"; + const [userStats, setUserStats] = useState(null); + const [statsLoading, setStatsLoading] = useState(true); + + useEffect(() => { + if (!user?.id) return; + getUserById(user.id) + .then(setUserStats) + .catch(() => setUserStats(null)) + .finally(() => setStatsLoading(false)); + }, [user?.id]); + + const xp = userStats?.xp ?? 0; + const level = userStats?.level ?? 1; + const rank = userStats?.rank ?? "—"; + const levelPct = userStats?.level_percentage ?? 0; + const nextLevelXp = level * 1000; + const xpToGo = nextLevelXp - xp; + const completed = userStats?.total_solved_quests ?? 0; + const completedPct = Math.round((completed / TOTAL_QUESTS) * 100); + return ( <> @@ -101,31 +125,27 @@ export default function Dashboard() { {/* ── Hero ── */}
-

- Dashboard -

Welcome back,{" "} - {displayName} + {displayName}

- Pick up where you left off. Your quests are waiting — keep - pushing your limits and climbing the leaderboard. + Your coding journey continues! Choose a quest to level up your skills, track your progress, and compete on the leaderboard. The adventure awaits!

{/* Quick stats */}
{[ - { label: "XP Points", value: "1,200" }, - { label: "Completed", value: "15" }, - { label: "Rank", value: "#24" }, + { label: "XP Points", value: statsLoading ? "…" : xp.toLocaleString() }, + { label: "Completed", value: statsLoading ? "…" : String(completed) }, + { label: "Rank", value: statsLoading ? "…" : rank }, ].map((stat) => (
-

+

{stat.value}

@@ -143,13 +163,13 @@ export default function Dashboard() { {languages.map((lang) => (

{/* Background image */} {lang.name} {/* Bottom gradient */} @@ -157,7 +177,7 @@ export default function Dashboard() { {/* Quest count badge */}
- + {lang.quests} quests
@@ -167,10 +187,10 @@ export default function Dashboard() {

{lang.name}

-

+

{lang.description}

-
+
Start quest
@@ -226,7 +246,9 @@ export default function Dashboard() {

XP Points

-

1,200 / 1,500 XP

+

+ {statsLoading ? "Loading…" : `${xp.toLocaleString()} / ${nextLevelXp.toLocaleString()} XP`} +

- 80% to next level - 300 XP to go + + {statsLoading ? "—" : `${levelPct}% to level ${level + 1}`} + + + {statsLoading ? "" : `${xpToGo.toLocaleString()} XP to go`} +
@@ -264,7 +290,9 @@ export default function Dashboard() {

Completed Challenges

-

15 of 20 completed

+

+ {statsLoading ? "Loading…" : `${completed} of ${TOTAL_QUESTS} completed`} +

- 75% complete - 5 remaining + + {statsLoading ? "—" : `${completedPct}% complete`} + + + {statsLoading ? "" : `${TOTAL_QUESTS - completed} remaining`} +
diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js index 0f36c42..1e6b851 100644 --- a/frontend/src/services/authService.js +++ b/frontend/src/services/authService.js @@ -79,13 +79,26 @@ export async function logout() { /** * Fetch the currently authenticated user from the backend. - * Returns the user object or null if not authenticated. + * If the access token is expired (401), attempts a silent refresh before + * giving up. Returns the user object or null if not authenticated. */ export async function getCurrentUser() { - const res = await fetch(`${API_BASE}/me`, { + let res = await fetch(`${API_BASE}/me`, { method: 'GET', credentials: 'include', }); + + if (res.status === 401) { + // Access token expired — try to get a new one from the refresh token + const refreshed = await refreshToken(); + if (!refreshed) return null; + // Retry with the fresh access token cookie + res = await fetch(`${API_BASE}/me`, { + method: 'GET', + credentials: 'include', + }); + } + if (!res.ok) return null; return res.json(); // { id, username, email } } From 20fb6734d5784a8fbf815ff51c3ea8c1e1cf248c Mon Sep 17 00:00:00 2001 From: Aleksandar Karastoyanov Date: Sun, 8 Mar 2026 10:08:48 +0200 Subject: [PATCH 2/3] Enhancement: UI rework: - Admin dashboard protected route implemented - Rework Dashboard and Admin dashboard UI styling Signed-off-by: Aleksandar Karastoyanov --- backend/routes/auth_routes.py | 4 +- frontend/src/App.jsx | 12 +- frontend/src/pages/AdminPanel.jsx | 202 +++++++++++++++++++----------- frontend/src/pages/Dashboard.jsx | 4 +- 4 files changed, 141 insertions(+), 81 deletions(-) diff --git a/backend/routes/auth_routes.py b/backend/routes/auth_routes.py index 159a06d..348649c 100644 --- a/backend/routes/auth_routes.py +++ b/backend/routes/auth_routes.py @@ -121,11 +121,13 @@ def login(): "id": user.id, "username": user.username, "email": user.email, + "role": user.role.role if user.role else "User", }, } ), 200, ) + # Tokens are stored in HttpOnly cookies — never exposed to JavaScript set_access_cookies(response, access_token) set_refresh_cookies(response, refresh_token) @@ -156,13 +158,13 @@ def me(): user = db.session.get(User, user_id) if not user: return jsonify({"error": "User not found"}), 404 - return ( jsonify( { "id": user.id, "username": user.username, "email": user.email, + "role": user.role.role if user.role else "User", } ), 200, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d90b125..d87c393 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -35,6 +35,14 @@ function PublicRoute({ children }) { return children; } +function AdminRoute({ children }) { + const { user, loading } = useAuth(); + if (loading) return ; + if (!user) return ; + if (user.role !== "Admin") return ; + return children; +} + export default function App() { return ( @@ -49,8 +57,8 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx index 23e595a..3dc9aaf 100644 --- a/frontend/src/pages/AdminPanel.jsx +++ b/frontend/src/pages/AdminPanel.jsx @@ -1,100 +1,150 @@ -/* eslint-disable no-unused-vars */ - -import React, {useState, useEffect} from "react"; +import { useState } from "react"; import Navbar from "../components/Layout/Navbar"; -import { getAllQuests } from "../services/questsServices"; - -// Admin Dashboard Components import Dashboard from "../components/Admin/Dashboard"; import AddQuest from "../components/Admin/AddQuest"; import EditQuest from "../components/Admin/EditQuest"; import NewBoss from "../components/Admin/NewBoss"; import QuestsLogs from "../components/Admin/QuestsLogs"; -const AdminPanel = () => { - const [activeSection, setActiveSection] = useState("dashboard"); +const NAV_ITEMS = [ + { + key: "dashboard", + label: "Dashboard", + icon: ( + + + + ), + }, + { + key: "add_quest", + label: "Add Quest", + icon: ( + + + + ), + }, + { + key: "edit_quest", + label: "Edit Quest", + icon: ( + + + + ), + }, + { + key: "underworld_boss", + label: "New Boss", + icon: ( + + + + + ), + }, + { + key: "quests_logs", + label: "Quests Logs", + icon: ( + + + + ), + }, +]; +function renderSection(activeSection) { + switch (activeSection) { + case "add_quest": return ; + case "edit_quest": return ; + case "underworld_boss": return ; + case "quests_logs": return ; + case "dashboard": + default: return ; + } +} - const renderSection = () => { - switch (activeSection) { - case "add_quest": - return ; - case "edit_quest": - return ; - case "underworld_boss": - return ; - case "quests_logs": - return ; - case "dashboard": - default: - return ; - } - }; +export default function AdminPanel() { + const [activeSection, setActiveSection] = useState("dashboard"); + const activeItem = NAV_ITEMS.find((i) => i.key === activeSection); return ( <> -
- {/* Sidebar */} -