| 1 |
'use client'; |
| 2 |
|
| 3 |
import { useState, useEffect } from 'react'; |
| 4 |
import Link from 'next/link'; |
| 5 |
import Header from '@/components/Header'; |
| 6 |
|
| 7 |
interface Person { |
| 8 |
id: number; |
| 9 |
display_name: string; |
| 10 |
rank: string; |
| 11 |
unit: string; |
| 12 |
} |
| 13 |
|
| 14 |
interface ConflictWithCasualties { |
| 15 |
id: number; |
| 16 |
name: string; |
| 17 |
start_year: number; |
| 18 |
end_year: number | null; |
| 19 |
description: string; |
| 20 |
casualty_count: number; |
| 21 |
casualties: Person[]; |
| 22 |
} |
| 23 |
|
| 24 |
export default function MemorialIndexPage() { |
| 25 |
const [conflicts, setConflicts] = useState<ConflictWithCasualties[]>([]); |
| 26 |
const [loading, setLoading] = useState(true); |
| 27 |
const [error, setError] = useState<string | null>(null); |
| 28 |
const [expandedConflicts, setExpandedConflicts] = useState<Set<number>>(new Set()); |
| 29 |
|
| 30 |
useEffect(() => { |
| 31 |
async function fetchData() { |
| 32 |
try { |
| 33 |
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; |
| 34 |
const response = await fetch(`${apiUrl}/memorial/index/`); |
| 35 |
|
| 36 |
if (!response.ok) { |
| 37 |
throw new Error('Failed to fetch memorial index'); |
| 38 |
} |
| 39 |
|
| 40 |
const data = await response.json(); |
| 41 |
setConflicts(data); |
| 42 |
|
| 43 |
// By default, expand conflicts with casualties |
| 44 |
const defaultExpanded = new Set( |
| 45 |
data.filter((c: ConflictWithCasualties) => c.casualty_count > 0).map((c: ConflictWithCasualties) => c.id) |
| 46 |
); |
| 47 |
setExpandedConflicts(defaultExpanded); |
| 48 |
} catch (err) { |
| 49 |
setError(err instanceof Error ? err.message : 'Failed to load data'); |
| 50 |
console.error(err); |
| 51 |
} finally { |
| 52 |
setLoading(false); |
| 53 |
} |
| 54 |
} |
| 55 |
|
| 56 |
fetchData(); |
| 57 |
}, []); |
| 58 |
|
| 59 |
const toggleConflict = (conflictId: number) => { |
| 60 |
const newExpanded = new Set(expandedConflicts); |
| 61 |
if (newExpanded.has(conflictId)) { |
| 62 |
newExpanded.delete(conflictId); |
| 63 |
} else { |
| 64 |
newExpanded.add(conflictId); |
| 65 |
} |
| 66 |
setExpandedConflicts(newExpanded); |
| 67 |
}; |
| 68 |
|
| 69 |
const toggleAll = () => { |
| 70 |
if (expandedConflicts.size === conflicts.length) { |
| 71 |
setExpandedConflicts(new Set()); |
| 72 |
} else { |
| 73 |
setExpandedConflicts(new Set(conflicts.map(c => c.id))); |
| 74 |
} |
| 75 |
}; |
| 76 |
|
| 77 |
if (loading) { |
| 78 |
return ( |
| 79 |
<div className="min-h-screen bg-vmi-cream flex items-center justify-center"> |
| 80 |
<p className="text-gray-600 text-xl">Loading memorial index...</p> |
| 81 |
</div> |
| 82 |
); |
| 83 |
} |
| 84 |
|
| 85 |
if (error) { |
| 86 |
return ( |
| 87 |
<div className="min-h-screen bg-vmi-cream flex items-center justify-center"> |
| 88 |
<div className="text-center"> |
| 89 |
<p className="text-red-600 mb-4 text-xl">{error}</p> |
| 90 |
<Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold"> |
| 91 |
Return to Home |
| 92 |
</Link> |
| 93 |
</div> |
| 94 |
</div> |
| 95 |
); |
| 96 |
} |
| 97 |
|
| 98 |
const totalCasualties = conflicts.reduce((sum, conflict) => sum + conflict.casualty_count, 0); |
| 99 |
|
| 100 |
return ( |
| 101 |
<div className="min-h-screen bg-vmi-cream"> |
| 102 |
<Header |
| 103 |
breadcrumbs={[ |
| 104 |
{ label: 'Home', href: '/' }, |
| 105 |
{ label: 'Complete Memorial Index' } |
| 106 |
]} |
| 107 |
showSearch={true} |
| 108 |
showIndex={false} |
| 109 |
/> |
| 110 |
|
| 111 |
{/* Main Content */} |
| 112 |
<main className="max-w-6xl mx-auto px-4 py-12"> |
| 113 |
{/* Index Header */} |
| 114 |
<div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl"> |
| 115 |
<h1 className="text-4xl font-black text-vmi-red mb-4"> |
| 116 |
VMI Memorial Index |
| 117 |
</h1> |
| 118 |
<p className="text-xl text-gray-700 mb-6"> |
| 119 |
Complete listing of all VMI alumni who made the ultimate sacrifice in service to their country. |
| 120 |
</p> |
| 121 |
<div className="flex flex-wrap gap-6 text-lg"> |
| 122 |
<div> |
| 123 |
<span className="font-bold text-gray-700">Total Conflicts:</span>{' '} |
| 124 |
<span className="text-vmi-red font-bold">{conflicts.length}</span> |
| 125 |
</div> |
| 126 |
<div> |
| 127 |
<span className="font-bold text-gray-700">Total Lives Given:</span>{' '} |
| 128 |
<span className="text-vmi-red font-bold">{totalCasualties}</span> |
| 129 |
</div> |
| 130 |
</div> |
| 131 |
<div className="mt-6"> |
| 132 |
<button |
| 133 |
onClick={toggleAll} |
| 134 |
className="bg-vmi-red text-white px-6 py-2 rounded hover:bg-vmi-dark-red transition-colors font-semibold" |
| 135 |
> |
| 136 |
{expandedConflicts.size === conflicts.length ? 'Collapse All' : 'Expand All'} |
| 137 |
</button> |
| 138 |
</div> |
| 139 |
</div> |
| 140 |
|
| 141 |
{/* Conflicts and Casualties */} |
| 142 |
<div className="space-y-8"> |
| 143 |
{conflicts.map((conflict) => ( |
| 144 |
<div key={conflict.id} className="bg-white border-2 border-gray-300 rounded-lg shadow-xl overflow-hidden"> |
| 145 |
{/* Conflict Header */} |
| 146 |
<div |
| 147 |
className="p-6 bg-gray-50 border-b-2 border-gray-300 cursor-pointer hover:bg-gray-100 transition-colors" |
| 148 |
onClick={() => toggleConflict(conflict.id)} |
| 149 |
> |
| 150 |
<div className="flex justify-between items-center"> |
| 151 |
<div className="flex-1"> |
| 152 |
<h2 className="text-2xl font-bold text-vmi-red mb-1"> |
| 153 |
{conflict.name} |
| 154 |
</h2> |
| 155 |
<p className="text-gray-600"> |
| 156 |
{conflict.start_year} – {conflict.end_year || 'Present'} |
| 157 |
</p> |
| 158 |
</div> |
| 159 |
<div className="flex items-center gap-4"> |
| 160 |
<div className="text-right"> |
| 161 |
<p className="text-3xl font-black text-vmi-red">{conflict.casualty_count}</p> |
| 162 |
<p className="text-sm text-gray-600 uppercase tracking-wide">Casualties</p> |
| 163 |
</div> |
| 164 |
<div className="text-2xl text-gray-400"> |
| 165 |
{expandedConflicts.has(conflict.id) ? '−' : '+'} |
| 166 |
</div> |
| 167 |
</div> |
| 168 |
</div> |
| 169 |
</div> |
| 170 |
|
| 171 |
{/* Casualties List */} |
| 172 |
{expandedConflicts.has(conflict.id) && conflict.casualty_count > 0 && ( |
| 173 |
<div className="p-6"> |
| 174 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
| 175 |
{conflict.casualties.map((person) => ( |
| 176 |
<Link |
| 177 |
key={person.id} |
| 178 |
href={`/memorial/person/${person.id}`} |
| 179 |
className="block p-4 border border-gray-200 rounded hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group" |
| 180 |
> |
| 181 |
<h3 className="font-bold text-gray-800 group-hover:text-vmi-red transition-colors"> |
| 182 |
{person.display_name} |
| 183 |
</h3> |
| 184 |
{person.rank && ( |
| 185 |
<p className="text-gray-700 text-sm">{person.rank}</p> |
| 186 |
)} |
| 187 |
{person.unit && ( |
| 188 |
<p className="text-gray-600 text-sm italic">{person.unit}</p> |
| 189 |
)} |
| 190 |
</Link> |
| 191 |
))} |
| 192 |
</div> |
| 193 |
</div> |
| 194 |
)} |
| 195 |
|
| 196 |
{/* No casualties message */} |
| 197 |
{expandedConflicts.has(conflict.id) && conflict.casualty_count === 0 && ( |
| 198 |
<div className="p-6 text-center text-gray-600"> |
| 199 |
<p>No casualties recorded for this conflict.</p> |
| 200 |
</div> |
| 201 |
)} |
| 202 |
</div> |
| 203 |
))} |
| 204 |
</div> |
| 205 |
</main> |
| 206 |
</div> |
| 207 |
); |
| 208 |
} |