113 lines
3.6 KiB
TypeScript
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>
|
|
)
|
|
}
|