Skip to content
Open
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
88 changes: 50 additions & 38 deletions frontend/app/users/[userId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,46 @@

import React, { useEffect, useState } from "react";

export default function PublicUserProfilePage({ params }: { params: { userId: string } }) {
const { userId } = params;
// --- FIX: Define specific types for the data ---
interface Solve {
id: number;
name: string;
points: number;
}

interface User {
id: number;
name: string;
team_name?: string;
points: number;
team_id?: number;
solves: Solve[];
}

interface Team {
id: number;
name: string;
points: number;
}

export default function PublicUserProfilePage({ params }: { params: { userId: string } }) {
const { userId } = params;

const [user, setUser] = useState<any>(null);
const [team, setTeam] = useState<any>(null);
// --- FIX: Use the new types instead of 'any' ---
const [user, setUser] = useState<User | null>(null);
const [team, setTeam] = useState<Team | null>(null);
const [teamRank, setTeamRank] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const API_BASE = "http://127.0.0.1:8000";

// ✅ UPDATED FUNCTION
function getAuthHeaders(): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};

const headers: Record<string, string> = { "Content-Type": "application/json" };
const token = localStorage.getItem("token");

if (token) {
headers["Authorization"] = `Bearer ${token}`;
} else {
console.warn("No auth token found. Continuing without authentication.");
}

return headers;
}

Expand All @@ -40,36 +54,36 @@ export default function PublicUserProfilePage({ params }: { params: { userId: st
try {
setLoading(true);

const userRes = await fetch(`${API_BASE}/users/${userId}`, {
headers: getAuthHeaders(),
});
if (!userRes.ok) {
throw new Error("User not found or failed to fetch data");
}
const userData = await userRes.json();
const userRes = await fetch(`${API_BASE}/users/${userId}`, { headers: getAuthHeaders() });
if (!userRes.ok) throw new Error("User not found or failed to fetch data");

// --- FIX: Tell TypeScript the shape of the incoming data ---
const userData: User = await userRes.json();
setUser(userData);

if (userData.team_id) {
const teamRes = await fetch(`${API_BASE}/teams/${userData.team_id}`, {
headers: getAuthHeaders(),
});
const teamRes = await fetch(`${API_BASE}/teams/${userData.team_id}`, { headers: getAuthHeaders() });
if (teamRes.ok) {
setTeam(await teamRes.json());
}

const allTeamsRes = await fetch(`${API_BASE}/teams`, {
headers: getAuthHeaders(),
});
const allTeamsRes = await fetch(`${API_BASE}/teams`, { headers: getAuthHeaders() });
if (allTeamsRes.ok) {
const allTeamsData = await allTeamsRes.json();
const teamsList = allTeamsData.teams || [];
const sortedTeams = teamsList.sort((a: any, b: any) => b.points - a.points);
const rank = sortedTeams.findIndex((t: any) => t.id === userData.team_id) + 1;
// --- FIX: Type the list of teams ---
const teamsList: Team[] = allTeamsData.teams || [];
const sortedTeams = teamsList.sort((a, b) => b.points - a.points);
const rank = sortedTeams.findIndex((t) => t.id === userData.team_id) + 1;
setTeamRank(rank > 0 ? rank : null);
}
}
} catch (err: any) {
setError(err.message);
} catch (err) {
// --- FIX: Type the error properly ---
if (err instanceof Error) {
setError(err.message);
} else {
setError("An unknown error occurred");
}
} finally {
setLoading(false);
}
Expand All @@ -85,14 +99,13 @@ export default function PublicUserProfilePage({ params }: { params: { userId: st
if (error) {
return <div className="flex justify-center items-center h-screen text-red-500">Error: {error}</div>;
}

if (!user) {
return <div className="flex justify-center items-center h-screen text-white">User not found.</div>;
}

return (
<div className="min-h-screen bg-[#221633] text-white px-4 py-10">
{/* Header */}
<div className="min-h-screen bg-[#221633] text-white px-4 py-10">
<div className="text-center mb-10">
<h1 className="text-4xl font-bold text-[#F34927] tracking-wide font-['Jaini_Purva']">
{user.team_name || "No Team"}
Expand All @@ -105,15 +118,13 @@ export default function PublicUserProfilePage({ params }: { params: { userId: st
)}
</div>

{/* User Info */}
<div className="text-center mb-10">
<h2 className="text-3xl font-semibold text-[#F34927] mb-2">
Username: {user.name}
</h2>
<p className="text-xl">Individual Points: {user.points}</p>
</div>

{/* Solves Table */}
<div className="max-w-5xl mx-auto bg-[#DDE6ED] rounded-xl shadow-md p-6">
<h2 className="text-3xl font-semibold text-center text-[#221633] font-['Jaini_Purva'] mb-6">
Solves
Expand All @@ -128,7 +139,8 @@ export default function PublicUserProfilePage({ params }: { params: { userId: st
</thead>
<tbody>
{user.solves && user.solves.length > 0 ? (
user.solves.map((solve: any) => (
// --- FIX: Use the 'Solve' type for each item in the map function ---
user.solves.map((solve: Solve) => (
<tr
key={solve.id}
className="bg-white text-black hover:bg-[#f2f2f2] transition rounded"
Expand Down Expand Up @@ -157,4 +169,4 @@ export default function PublicUserProfilePage({ params }: { params: { userId: st
</div>
</div>
);
}
}
32 changes: 17 additions & 15 deletions frontend/app/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// File: app/users/page.tsx

"use client";

import React, { useEffect, useState } from "react";
import { useRouter } from 'next/navigation'; // ✅ 1. Import the useRouter hook
import { Jersey_10, Jaini_Purva, Outfit } from "next/font/google";
import { useRouter } from 'next/navigation';
// --- FIX: Removed 'Jaini_Purva' as it was unused ---
import { Jersey_10, Outfit } from "next/font/google";

const jersey = Jersey_10({ subsets: ["latin"], weight: "400", variable: "--font-jersey-10" });
const jainiPurva = Jaini_Purva({ subsets: ["latin"], weight: "400", variable: "--font-jaini-purva" });
const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" });

interface User {
Expand All @@ -18,25 +20,29 @@ interface User {
}

export default function UsersPage() {
const router = useRouter(); // ✅ 2. Initialize the router
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [filterBy, setFilterBy] = useState<"name" | "affiliation" | "country">("name");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");

const API_BASE = "http://127.0.0.1:8000";

useEffect(() => {
const fetchUsers = async () => {
try {
// Using your backend endpoint to get the list of all users
const res = await fetch(`${API_BASE}/users`);
if (!res.ok) throw new Error("Failed to fetch users");
const data = await res.json();
setUsers(data.users || []);
} catch (err: any) {
setError(err.message || "Error fetching users");
} catch (err) {
// --- FIX: Typed the error properly instead of using 'any' ---
if (err instanceof Error) {
setError(err.message);
} else {
setError("An unknown error occurred while fetching users");
}
} finally {
setLoading(false);
}
Expand All @@ -48,12 +54,11 @@ export default function UsersPage() {
const filteredUsers = users.filter((user) => {
const field =
filterBy === "name" ? user.name :
filterBy === "affiliation" ? user?.affiliation ?? "" :
user?.country ?? "";
filterBy === "affiliation" ? user?.affiliation ?? "" :
user?.country ?? "";
return field.toLowerCase().includes(searchTerm.toLowerCase());
});

// Function to handle navigation when a row is clicked
const handleRowClick = (userId: number) => {
router.push(`/users/${userId}`);
};
Expand All @@ -66,7 +71,6 @@ export default function UsersPage() {
</h1>
</div>

{/* Search and Filter */}
<div className="flex justify-center items-center gap-2 mb-8 flex-wrap">
<div className={outfit.variable}>
<select
Expand All @@ -93,7 +97,6 @@ export default function UsersPage() {
/>
</div>

{/* Table */}
{loading ? (
<p className="text-center text-gray-400">Loading users...</p>
) : error ? (
Expand All @@ -110,7 +113,6 @@ export default function UsersPage() {
</thead>
<tbody>
{filteredUsers.map((user) => (
// ✅ 3. Add onClick handler and cursor-pointer for better UX
<tr
key={user.id}
className="hover:bg-[#3a2c52] rounded-xl transition-all cursor-pointer"
Expand All @@ -133,4 +135,4 @@ export default function UsersPage() {
)}
</div>
);
}
}
65 changes: 33 additions & 32 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
{
"name": "flagged-ctfd",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@tsparticles/engine": "^3.8.1",
"@tsparticles/react": "^3.0.0",
"@vscode/codicons": "^0.0.36",
"lucide-react": "^0.525.0",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsparticles": "^3.8.1"
},
"devDependencies": {
"@types/node": "^20.17.5",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"postcss": "^8.5.4",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
}
}
{
"name": "flagged-ctfd",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@tsparticles/engine": "^3.8.1",
"@tsparticles/react": "^3.0.0",
"@vscode/codicons": "^0.0.36",
"lucide-react": "^0.525.0",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsparticles": "^3.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/node": "^20.17.5",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"postcss": "^8.5.4",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
}
}
3 changes: 3 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.