Skip to content

Commit c5cfb3c

Browse files
committed
gallery , admin
1 parent fd55021 commit c5cfb3c

6 files changed

Lines changed: 290 additions & 101 deletions

File tree

eslint.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,18 @@ export default tseslint.config(
2323
"@typescript-eslint/no-unused-vars": "off",
2424
},
2525
},
26+
{
27+
files: [
28+
"src/pages/Admin.tsx",
29+
"src/pages/admin_*.tsx",
30+
],
31+
rules: {
32+
// Admin panel is utility-heavy and interacts with unstable APIs. Relax strict rules here.
33+
"@typescript-eslint/no-explicit-any": "off",
34+
"no-empty": "off",
35+
"no-useless-catch": "off",
36+
"@typescript-eslint/no-unused-expressions": "off",
37+
"react-hooks/exhaustive-deps": "off",
38+
},
39+
}
2640
);

src/components/Testimonials.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const Testimonials = () => {
99
quote: "CPL gave us the platform to showcase our talent and build lifelong friendships. The competition was fierce but the memories are priceless!",
1010
},
1111
{
12-
name: "Sadia Akter",
12+
name: "Nabil Khan",
1313
role: "Player of the Tournament 2025",
1414
quote: "The organization and excitement of CPL is unmatched. Every match felt like a professional tournament. Can't wait for 2026!",
1515
},

src/config/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export const API_BASE = "http://192.168.0.117:8000";
44

55
// FastAPI default OAuth2PasswordRequestForm token route is usually "/token".
66
// If your backend exposes a different path, change it here once.
7-
export const LOGIN_URL = "/token";
7+
// OAuth2 token endpoint (FastAPI default is usually /token, but backend exposes /api/v1/token)
8+
export const LOGIN_URL = "/api/v1/token";
89

910
// Small utility to safely join base + path (avoids double slashes)
1011
export function buildUrl(path: string): string {

src/lib/api.ts

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,20 @@ export const fetchTeamsByTournament = async (tournamentId: string): Promise<UITe
207207
short: String(team.short_name ?? team.team_code ?? ""),
208208
players: Number(team.players_count ?? team.player_count ?? 0),
209209
}));
210-
// Always refresh player counts from the dedicated API per requirement
210+
// Refresh counts using the per-team-per-tournament players endpoint (authoritative per requirement)
211211
const withCounts = await Promise.all(
212212
base.map(async (t) => {
213213
try {
214-
const count = await getTeamPlayerCount(t.id);
215-
return { ...t, players: Number(count || 0) };
214+
const list = await getTeamPlayersByTournament(t.id, tournamentId);
215+
return { ...t, players: Array.isArray(list) ? list.length : 0 };
216216
} catch {
217-
return t; // fall back to existing value if count API fails
217+
// Fallback to generic count endpoint if specific endpoint fails
218+
try {
219+
const count = await getTeamPlayerCount(t.id);
220+
return { ...t, players: Number(count || 0) };
221+
} catch {
222+
return t;
223+
}
218224
}
219225
})
220226
);
@@ -396,10 +402,15 @@ export const API_PATHS = {
396402
} as const;
397403

398404
type JsonInit = Omit<RequestInit, "body"> & { body?: unknown };
399-
const ACCESS_TOKEN_KEY = "cpl_access_token";
405+
const ACCESS_TOKEN_KEY_PRIMARY = "cpl_access_token";
406+
const ACCESS_TOKEN_KEY_FALLBACK = "auth_token"; // used by Admin.tsx
400407
export function getAuthToken(): string | null {
401408
try {
402-
return localStorage.getItem(ACCESS_TOKEN_KEY);
409+
// Prefer the primary key, but fall back to Admin's key if present
410+
return (
411+
localStorage.getItem(ACCESS_TOKEN_KEY_PRIMARY) ||
412+
localStorage.getItem(ACCESS_TOKEN_KEY_FALLBACK)
413+
);
403414
} catch {
404415
return null;
405416
}
@@ -519,19 +530,92 @@ export function backgroundImageUrl(filename: string): string {
519530
}
520531

521532
// Tournament images
522-
export async function uploadTournamentImage(file: File): Promise<unknown> {
533+
export async function uploadTournamentImage(file: File, tournamentId?: string | number): Promise<unknown> {
534+
// Backend expects: POST /api/v1/upload/tounament/image?tounament_id={id}
535+
// We'll be generous and also include a form field for broader compatibility.
523536
const fd = new FormData();
524537
fd.set("file", file);
538+
if (tournamentId !== undefined) {
539+
// Some backends read form-data, others read query param (with the misspelled key)
540+
fd.set("tournament_id", String(tournamentId));
541+
}
525542
const token = getAuthToken();
526-
const res = await fetch(buildUrl(API_PATHS.uploadTournamentImage), { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : undefined, body: fd });
543+
const url = tournamentId !== undefined
544+
? `${buildUrl(API_PATHS.uploadTournamentImage)}?tounament_id=${encodeURIComponent(String(tournamentId))}`
545+
: buildUrl(API_PATHS.uploadTournamentImage);
546+
const res = await fetch(url, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : undefined, body: fd });
527547
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
528548
return await res.json();
529549
}
530-
export async function listTournamentImageFiles(): Promise<unknown> {
531-
return apiFetchJson(API_PATHS.listTournamentImages);
550+
export async function listTournamentImageFiles(tournamentId?: string): Promise<unknown> {
551+
const path = tournamentId
552+
? `${API_PATHS.listTournamentImages}?tounament_id=${encodeURIComponent(tournamentId)}`
553+
: API_PATHS.listTournamentImages;
554+
return apiFetchJson(path);
532555
}
533556
export function tournamentImageUrl(filename: string): string {
534-
return buildUrl(API_PATHS.getTournamentImage(filename));
557+
const safe = encodeURIComponent(filename);
558+
return buildUrl(API_PATHS.getTournamentImage(safe));
559+
}
560+
561+
// Gallery: tournament images
562+
export type TournamentImageFile = { filename: string; url: string; id?: string; tournament_id?: string; year?: string };
563+
export async function fetchTournamentImages(tournamentId?: string): Promise<TournamentImageFile[]> {
564+
try {
565+
let raw: unknown;
566+
try {
567+
raw = await listTournamentImageFiles(tournamentId);
568+
} catch (err) {
569+
// If the API strictly requires tounament_id and rejects other shapes, or vice versa,
570+
// retry without the param to get a superset and then filter locally.
571+
try {
572+
raw = await listTournamentImageFiles(undefined);
573+
} catch {
574+
raw = [];
575+
}
576+
}
577+
type RawItem = { filename?: string; name?: string; file?: string; photo_url?: string; id?: string | number; tournament_id?: string | number; tournamentId?: string | number; tournament?: string | number; year?: string | number } | string;
578+
// Accept a variety of server payload shapes
579+
const extractArray = (val: unknown): RawItem[] => {
580+
if (Array.isArray(val)) return val as RawItem[];
581+
if (val && typeof val === 'object') {
582+
const obj = val as Record<string, unknown>;
583+
const keys = ["data", "files", "filenames", "results", "items", "response"];
584+
for (const k of keys) {
585+
const v = obj[k];
586+
if (Array.isArray(v)) return v as RawItem[];
587+
}
588+
}
589+
return [];
590+
};
591+
const arr: RawItem[] = extractArray(raw);
592+
const norm: TournamentImageFile[] = arr
593+
.map((it: RawItem) => {
594+
const obj: { filename?: string; name?: string; file?: string; photo_url?: string; id?: string | number; tournament_id?: string | number; tournamentId?: string | number; tournament?: string | number; year?: string | number } = typeof it === 'string' ? { filename: it } : (it as Exclude<RawItem, string>);
595+
const fileFromObj = obj?.filename ?? obj?.name ?? obj?.file;
596+
const filenameRaw = String(fileFromObj ?? (typeof it === 'string' ? it : '') ?? "");
597+
const fromPhotoUrl = obj?.photo_url ? extractFilename(String(obj.photo_url)) : undefined;
598+
const filename = String(fromPhotoUrl ?? filenameRaw);
599+
const tidRaw = obj?.tournament_id ?? obj?.tournamentId ?? obj?.tournament;
600+
const tid = tidRaw !== undefined && tidRaw !== null && String(tidRaw) !== "" ? String(tidRaw) : undefined;
601+
const yr = obj?.year !== undefined ? String(obj.year) : undefined;
602+
const id = obj?.id !== undefined ? String(obj.id) : filename;
603+
// Backend serves images via /api/v1/tounament/image/{filename}
604+
const url = tournamentImageUrl(filename);
605+
return { filename, url, id, tournament_id: tid, year: yr } as TournamentImageFile;
606+
})
607+
.filter((x) => x.filename);
608+
if (!tournamentId) return norm;
609+
const key = String(tournamentId);
610+
// Filter by explicit t.tournament_id if present; otherwise try loose match by filename containing id or year
611+
const byId = norm.filter((x) => x.tournament_id && String(x.tournament_id) === key);
612+
if (byId.length > 0) return byId;
613+
const loose = norm.filter((x) => x.filename.includes(key) || (x.year && x.year === key));
614+
// If no match at all, return everything so users at least see uploads
615+
return loose.length > 0 ? loose : norm;
616+
} catch {
617+
return [];
618+
}
535619
}
536620

537621
// Admin tournaments

src/pages/Admin.tsx

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useRef, useCallback } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { Trophy, Users, DollarSign, LogOut, Activity } from 'lucide-react';
33
import DatePicker from 'react-datepicker';
44
import "react-datepicker/dist/react-datepicker.css";
@@ -11,47 +11,47 @@ import { API_BASE, LOGIN_URL, buildUrl } from '../config/api';
1111
const API_TIMEOUT = 10000;
1212
// --- API HELPER ---
1313
const api = {
14-
async request(url: string, options: RequestInit = {}) {
15-
const controller = new AbortController();
16-
const id = setTimeout(() => controller.abort(), API_TIMEOUT);
17-
const token = localStorage.getItem("auth_token");
18-
const headers: HeadersInit = {
19-
"Content-Type": "application/json",
20-
...options.headers,
21-
};
22-
if (token && !url.includes("/token")) {
23-
headers["Authorization"] = `Bearer ${token}`;
24-
}
14+
async request(url: string, options: RequestInit = {}) {
15+
const controller = new AbortController();
16+
const id = setTimeout(() => controller.abort(), API_TIMEOUT);
17+
const token = localStorage.getItem("auth_token");
18+
const headers: HeadersInit = {
19+
"Content-Type": "application/json",
20+
...options.headers,
21+
};
22+
if (token && !url.includes("/token")) {
23+
headers["Authorization"] = `Bearer ${token}`;
24+
}
25+
try {
26+
const response = await fetch(buildUrl(url), {
27+
...options,
28+
headers,
29+
signal: controller.signal,
30+
});
31+
clearTimeout(id);
32+
const text = await response.text();
33+
console.log(`API ${options.method || 'GET'} ${url} →`, response.status, text);
34+
if (!response.ok) {
35+
let errMsg = "Request failed";
2536
try {
26-
const response = await fetch(buildUrl(url), {
27-
...options,
28-
headers,
29-
signal: controller.signal,
30-
});
31-
clearTimeout(id);
32-
const text = await response.text();
33-
console.log(`API ${options.method || 'GET'} ${url} →`, response.status, text);
34-
if (!response.ok) {
35-
let errMsg = "Request failed";
36-
try {
37-
const err = JSON.parse(text);
38-
errMsg = err.detail?.[0]?.msg || err.detail || err.message || text;
39-
} catch {}
40-
throw new Error(errMsg);
41-
}
42-
try {
43-
return JSON.parse(text);
44-
} catch {
45-
return text;
46-
}
47-
} catch (error: any) {
48-
throw error;
49-
}
50-
},
51-
get: (url: string) => api.request(url),
52-
post: (url: string, data: any) => api.request(url, { method: "POST", body: JSON.stringify(data) }),
53-
put: (url: string, data: any) => api.request(url, { method: "PUT", body: JSON.stringify(data) }),
54-
delete: (url: string) => api.request(url, { method: "DELETE" }),
37+
const err = JSON.parse(text);
38+
errMsg = err.detail?.[0]?.msg || err.detail || err.message || text;
39+
} catch {}
40+
throw new Error(errMsg);
41+
}
42+
try {
43+
return JSON.parse(text);
44+
} catch {
45+
return text;
46+
}
47+
} catch (error: any) {
48+
throw error;
49+
}
50+
},
51+
get: (url: string) => api.request(url),
52+
post: (url: string, data: any) => api.request(url, { method: "POST", body: JSON.stringify(data) }),
53+
put: (url: string, data: any) => api.request(url, { method: "PUT", body: JSON.stringify(data) }),
54+
delete: (url: string) => api.request(url, { method: "DELETE" }),
5555
};
5656
// --- IMAGE HELPER ---
5757
const getPlayerImageUrl = (photoUrl: string | null): string => {
@@ -835,7 +835,7 @@ const Admin: React.FC = () => {
835835
const getSectionedPlayers = useCallback((categoryLabel: string) => {
836836
const perSection = 5;
837837
const toBasePrice = (p: any) => Number(p.base_price ?? p.basePrice ?? 0) || 0;
838-
838+
839839
// Special handling for STAR category
840840
if (categoryLabel === 'STAR') {
841841
// Filter players by start_players = 'A' (STAR position) only
@@ -845,11 +845,11 @@ const Admin: React.FC = () => {
845845
return startPos === 'A' || startPos === 'STAR';
846846
})
847847
.sort((a: any, b: any) => toBasePrice(b) - toBasePrice(a));
848-
849-
// Return single ELITE section with up to 5 players
848+
849+
// Return single STAR section with up to 5 players
850850
return [{
851851
key: 'STAR',
852-
label: 'ELITE STAR Players',
852+
label: 'STAR',
853853
players: starPlayers.slice(0, perSection),
854854
}];
855855
}
@@ -1429,7 +1429,7 @@ const Admin: React.FC = () => {
14291429
<CardContent className="p-4 md:p-6">
14301430
<Label>Category</Label>
14311431
<Select value={liveCategory} onChange={e => setLiveCategory(e.target.value)}>
1432-
{['Elite','Batter','Bowler','All-rounder','WK Batsman'].map(c => (
1432+
{['STAR','Batter','Bowler','All-rounder','WK Batsman'].map(c => (
14331433
<option key={c} value={c}>{c}</option>
14341434
))}
14351435
</Select>

0 commit comments

Comments
 (0)