page fixes

This commit is contained in:
root
2026-06-11 03:22:22 -04:00
parent fe1c50878b
commit e8d1b8c1c7
7 changed files with 798 additions and 235 deletions
@@ -1,85 +1,101 @@
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { fetchPublishedCompetition } from '../../api/session'
import type { PublicCompetitionRow, PublicCompetitionWinnerRow } from '../../api/types'
import { fetchAdminCompetitionWinnerPreview, publishAdminCompetitionWinner } from '../../api/session'
import type { CompetitionWinnerAdminPreviewResponse, CompetitionWinnerPreviewStudentRow } from '../../api/types'
import { ApiScopeNote, CompetitionPageShell, competitionLabel } from './competitionWinnersShared'
function usePublishedCompetition(id: number) {
const [competition, setCompetition] = useState<PublicCompetitionRow | null>(null)
const [winnersByClass, setWinnersByClass] = useState<Record<string, PublicCompetitionWinnerRow[]>>({})
const [sectionMap, setSectionMap] = useState<Record<string, string>>({})
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!Number.isFinite(id) || id <= 0) return
;(async () => {
try {
const response = await fetchPublishedCompetition(id)
if (!response.status || !response.data) {
setError(response.message ?? 'Competition not found.')
return
}
setCompetition(response.data.competition)
setWinnersByClass(response.data.winners_by_class ?? {})
setSectionMap(response.data.section_map ?? {})
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load winners.')
}
})()
}, [id])
return { competition, winnersByClass, sectionMap, error }
}
/** CI `admin/competition_winners/preview.php` */
export function CompetitionWinnerPreviewPage() {
const { id } = useParams()
const competitionId = Number(id)
const { competition, winnersByClass, sectionMap, error } = usePublishedCompetition(competitionId)
const [data, setData] = useState<CompetitionWinnerAdminPreviewResponse | null>(null)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [publishing, setPublishing] = useState(false)
async function load() {
try {
const response = await fetchAdminCompetitionWinnerPreview(competitionId)
setData(response)
setError(response.ok ? null : response.message ?? 'Competition not found.')
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to load winners preview.')
}
}
useEffect(() => {
if (!Number.isFinite(competitionId) || competitionId <= 0) return
void load()
}, [competitionId])
async function handlePublish() {
try {
setPublishing(true)
setMessage(null)
const result = await publishAdminCompetitionWinner(competitionId)
if (!result.ok) {
setError(result.message ?? 'Unable to publish competition winners.')
return
}
setMessage(result.message ?? 'Competition winners published.')
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Unable to publish competition winners.')
} finally {
setPublishing(false)
}
}
return (
<CompetitionPageShell title="Preview Winners">
<ApiScopeNote>
Preview reflects published winners from the public API. Unpublished drafts are not shown until the backend exposes an
admin preview.
Preview is generated from the admin scoring data, so it works before a competition is publicly published.
</ApiScopeNote>
{error ? <div className="alert alert-danger">{error}</div> : null}
{competition ? (
{message ? <div className="alert alert-success">{message}</div> : null}
{data?.competition ? (
<>
<div className="mb-3">
<div className="mb-3 d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<strong>Competition:</strong> {competitionLabel(competition)}
</div>
<div>
<strong>School Year:</strong> {competition.school_year ?? '—'}
<div>
<strong>Competition:</strong> {competitionLabel(data.competition)}
</div>
<div>
<strong>School Year:</strong> {data.competition.school_year ?? '—'}
</div>
<div>
<strong>Published:</strong> {data.competition.is_published ? 'Yes' : 'No'}
</div>
</div>
<button type="button" className="btn btn-primary" disabled={publishing} onClick={() => void handlePublish()}>
{publishing ? 'Publishing...' : 'Publish Winners'}
</button>
</div>
{Object.keys(winnersByClass).length === 0 ? (
<div className="alert alert-warning">No published winners found for this competition.</div>
{Object.keys(data.topByClass ?? {}).length === 0 ? (
<div className="alert alert-warning">No ranked winners found for this competition yet.</div>
) : (
Object.entries(winnersByClass).map(([classId, rows]) => (
Object.entries(data.topByClass ?? {}).map(([classId, rows]) => (
<div key={classId} className="mb-4">
<h5 className="mt-4">Top Winners - {sectionMap[classId] ?? `Class ${classId}`}</h5>
<h5 className="mt-4">
Top Winners - {data.classMap?.[classId] ?? `Class ${classId}`}
</h5>
<table className="table table-bordered table-sm">
<thead className="table-light">
<tr>
<th>Rank</th>
<th>Student</th>
<th>Score</th>
<th>Question Count</th>
<th>Prize</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
{(rows as CompetitionWinnerPreviewStudentRow[]).map((row, index) => (
<tr key={`${classId}-${index}`}>
<td>{row.rank ?? index + 1}</td>
<td>
{row.name ??
(`${row.firstname ?? ''} ${row.lastname ?? ''}`.trim() ||
`Student #${row.student_id ?? ''}`)}
</td>
<td>{row.name ?? `Student #${row.student_id ?? ''}`}</td>
<td>{row.score ?? '—'}</td>
<td>{data.questionCounts?.[classId] ?? '—'}</td>
<td>{row.prize_amount ?? '—'}</td>
</tr>
))}