rcnn/web/src/challenges/ChallengePanel.tsx
2026-04-25 23:15:46 +02:00

113 lines
3.6 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Panel } from '../ui/Panel'
import { useMapLibreMap } from '../maplibre/maplibreContext'
import { useChallengeStore } from '../store/challengeStore'
import { fetchChallengeDetail, participateInChallenge } from '../api/challenges'
import type { ChallengeDetail } from '../types/api'
import styles from './ChallengePanel.module.css'
export function ChallengePanel() {
const map = useMapLibreMap()
const navigate = useNavigate()
const { selectedChallengeId, setSelectedChallengeId } = useChallengeStore()
const [detail, setDetail] = useState<ChallengeDetail | null>(null)
const [participating, setParticipating] = useState(false)
useEffect(() => {
if (!selectedChallengeId) {
setDetail(null)
return
}
fetchChallengeDetail(selectedChallengeId)
.then((d) => {
setDetail(d)
setParticipating(d.is_participating)
})
.catch(console.error)
}, [selectedChallengeId])
function handleCenterMap() {
if (!detail?.region_centroid) return
const [lon, lat] = detail.region_centroid.coordinates
map.flyTo({ center: [lon, lat], zoom: 14, pitch: 50, duration: 1500 })
}
async function handleParticipate() {
if (!selectedChallengeId) return
await participateInChallenge(selectedChallengeId)
setParticipating(true)
}
return (
<Panel
open={!!selectedChallengeId}
onClose={() => setSelectedChallengeId(null)}
title={detail?.title ?? 'Challenge'}
>
{!detail ? (
<p className={styles.loading}>Loading</p>
) : (
<div className={styles.content}>
{detail.description && (
<p className={styles.description}>{detail.description}</p>
)}
<dl className={styles.meta}>
<dt>Created by</dt>
<dd>{detail.creator_username}</dd>
<dt>Submissions</dt>
<dd>
{detail.submission_count}
{detail.max_submissions ? ` / ${detail.max_submissions}` : ''}
</dd>
{detail.expires_at && (
<>
<dt>Expires</dt>
<dd>{new Date(detail.expires_at).toLocaleDateString()}</dd>
</>
)}
</dl>
<button className={styles.centerBtn} onClick={handleCenterMap}>
Center map
</button>
<button
className={styles.coasterBtn}
onClick={() => navigate(`/challenges/${selectedChallengeId}/coaster`)}
>
Plan coaster route
</button>
{detail.status === 'active' && !participating && (
<button className={styles.participateBtn} onClick={handleParticipate}>
Accept challenge
</button>
)}
{participating && (
<p className={styles.participating}>You've accepted this challenge</p>
)}
{detail.preview_splats.length > 0 && (
<section className={styles.previews}>
<h3>Submissions</h3>
<div className={styles.previewGrid}>
{detail.preview_splats.map((s) => (
<div key={s.id} className={styles.previewCard}>
{s.preview_url ? (
<img src={s.preview_url} alt="Splat preview" />
) : (
<div className={styles.previewPlaceholder}>3D</div>
)}
</div>
))}
</div>
</section>
)}
</div>
)}
</Panel>
)
}