TypeScript · 7543 bytes Raw Blame History
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 }