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