2026-02-09 11:00:37 +01:00
|
|
|
import { useMemo, useState } from 'react'
|
|
|
|
|
import { useGenlockeGraveyard } from '../hooks/useGenlockes'
|
|
|
|
|
import { TypeBadge } from './TypeBadge'
|
|
|
|
|
import type { GraveyardEntry } from '../types'
|
|
|
|
|
|
|
|
|
|
type SortKey = 'leg' | 'level' | 'species'
|
|
|
|
|
|
|
|
|
|
interface GenlockeGraveyardProps {
|
|
|
|
|
genlockeId: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GraveyardCard({ entry }: { entry: GraveyardEntry }) {
|
|
|
|
|
const displayPokemon = entry.currentPokemon ?? entry.pokemon
|
|
|
|
|
const isEvolved = entry.currentPokemon !== null
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="bg-surface-1 rounded-lg shadow p-4 flex flex-col items-center text-center opacity-60 grayscale">
|
2026-02-09 11:00:37 +01:00
|
|
|
{displayPokemon.spriteUrl ? (
|
|
|
|
|
<img
|
|
|
|
|
src={displayPokemon.spriteUrl}
|
|
|
|
|
alt={displayPokemon.name}
|
|
|
|
|
className="w-25 h-25"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="w-25 h-25 rounded-full bg-surface-3 flex items-center justify-center text-xl font-bold text-text-secondary">
|
2026-02-16 20:39:41 +01:00
|
|
|
{displayPokemon.name[0]?.toUpperCase()}
|
2026-02-09 11:00:37 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="mt-2 flex items-center gap-1.5">
|
|
|
|
|
<span className="w-2 h-2 rounded-full shrink-0 bg-red-500" />
|
2026-02-17 20:47:26 +01:00
|
|
|
<span className="font-semibold text-text-primary text-sm">
|
2026-02-09 11:00:37 +01:00
|
|
|
{entry.nickname || displayPokemon.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-02-17 20:47:26 +01:00
|
|
|
{entry.nickname && <div className="text-xs text-text-tertiary">{displayPokemon.name}</div>}
|
2026-02-09 11:00:37 +01:00
|
|
|
|
|
|
|
|
<div className="flex flex-col items-center gap-0.5 mt-1">
|
|
|
|
|
{displayPokemon.types.map((type) => (
|
|
|
|
|
<TypeBadge key={type} type={type} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="text-xs text-text-tertiary mt-1">
|
2026-02-09 11:00:37 +01:00
|
|
|
Lv. {entry.catchLevel} → {entry.faintLevel}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="text-xs text-text-muted mt-0.5">{entry.routeName}</div>
|
2026-02-09 11:00:37 +01:00
|
|
|
|
|
|
|
|
<div className="text-[10px] text-purple-600 dark:text-purple-400 mt-0.5 font-medium">
|
|
|
|
|
Leg {entry.legOrder} — {entry.gameName}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isEvolved && (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="text-[10px] text-text-muted mt-0.5">Originally: {entry.pokemon.name}</div>
|
2026-02-09 11:00:37 +01:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{entry.deathCause && (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="text-[10px] italic text-text-muted mt-0.5 line-clamp-2">
|
2026-02-09 11:00:37 +01:00
|
|
|
{entry.deathCause}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) {
|
|
|
|
|
const { data, isLoading, error } = useGenlockeGraveyard(genlockeId)
|
|
|
|
|
const [filterLeg, setFilterLeg] = useState<number | null>(null)
|
|
|
|
|
const [sortKey, setSortKey] = useState<SortKey>('leg')
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
if (!data) return []
|
|
|
|
|
let entries = data.entries
|
|
|
|
|
if (filterLeg !== null) {
|
|
|
|
|
entries = entries.filter((e) => e.legOrder === filterLeg)
|
|
|
|
|
}
|
|
|
|
|
return [...entries].sort((a, b) => {
|
|
|
|
|
switch (sortKey) {
|
|
|
|
|
case 'leg':
|
|
|
|
|
return a.legOrder - b.legOrder || a.id - b.id
|
|
|
|
|
case 'level':
|
|
|
|
|
return (b.faintLevel ?? 0) - (a.faintLevel ?? 0)
|
|
|
|
|
case 'species': {
|
|
|
|
|
const nameA = (a.currentPokemon ?? a.pokemon).name
|
|
|
|
|
const nameB = (b.currentPokemon ?? b.pokemon).name
|
|
|
|
|
return nameA.localeCompare(nameB)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}, [data, filterLeg, sortKey])
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center py-8">
|
|
|
|
|
<div className="w-6 h-6 border-4 border-red-600 border-t-transparent rounded-full animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
2026-02-09 11:00:37 +01:00
|
|
|
Failed to load graveyard data.
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data || data.totalDeaths === 0) {
|
|
|
|
|
return (
|
2026-02-17 20:47:26 +01:00
|
|
|
<div className="rounded-lg bg-surface-1/50 p-6 text-center text-text-tertiary">
|
2026-02-09 11:00:37 +01:00
|
|
|
No deaths recorded across any leg.
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Summary bar */}
|
|
|
|
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
2026-02-17 20:47:26 +01:00
|
|
|
<span className="font-semibold text-text-primary">
|
2026-02-09 11:00:37 +01:00
|
|
|
{data.totalDeaths} total death{data.totalDeaths !== 1 ? 's' : ''}
|
|
|
|
|
</span>
|
|
|
|
|
{data.deadliestLeg && (
|
2026-02-17 20:47:26 +01:00
|
|
|
<span className="text-text-tertiary">
|
2026-02-16 20:39:41 +01:00
|
|
|
Deadliest: Leg {data.deadliestLeg.legOrder} — {data.deadliestLeg.gameName} (
|
|
|
|
|
{data.deadliestLeg.deathCount})
|
2026-02-09 11:00:37 +01:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Controls */}
|
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
|
|
|
<select
|
|
|
|
|
value={filterLeg ?? ''}
|
2026-02-16 20:39:41 +01:00
|
|
|
onChange={(e) => setFilterLeg(e.target.value ? Number(e.target.value) : null)}
|
2026-02-17 20:47:26 +01:00
|
|
|
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
|
2026-02-09 11:00:37 +01:00
|
|
|
>
|
|
|
|
|
<option value="">All Legs</option>
|
|
|
|
|
{data.deathsPerLeg.map((leg) => (
|
|
|
|
|
<option key={leg.legOrder} value={leg.legOrder}>
|
|
|
|
|
Leg {leg.legOrder} — {leg.gameName} ({leg.deathCount})
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
value={sortKey}
|
|
|
|
|
onChange={(e) => setSortKey(e.target.value as SortKey)}
|
2026-02-17 20:47:26 +01:00
|
|
|
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
|
2026-02-09 11:00:37 +01:00
|
|
|
>
|
|
|
|
|
<option value="leg">Sort by Leg</option>
|
|
|
|
|
<option value="level">Sort by Level</option>
|
|
|
|
|
<option value="species">Sort by Species</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Grid */}
|
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
|
|
|
{filtered.map((entry) => (
|
|
|
|
|
<GraveyardCard key={entry.id} entry={entry} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|