TypeScript · 13498 bytes Raw Blame History
1 'use client';
2
3 import { useState, useEffect } from 'react';
4 import Link from 'next/link';
5 import { searchPeople, getSearchFilters, PersonDetail, SearchFilters } from '@/lib/api';
6 import Header from '@/components/Header';
7 import DocumentIcon from '@/components/DocumentIcon';
8 import AwardIcon from '@/components/AwardIcon';
9
10 export default function MemorialSearchPage() {
11 // Search state
12 const [searchTerm, setSearchTerm] = useState('');
13 const [selectedClassYears, setSelectedClassYears] = useState<number[]>([]);
14 const [selectedConflicts, setSelectedConflicts] = useState<number[]>([]);
15 const [dateFrom, setDateFrom] = useState('');
16 const [dateTo, setDateTo] = useState('');
17 const [noDate, setNoDate] = useState(false);
18 const [hasDocument, setHasDocument] = useState(false);
19
20 // Results state
21 const [results, setResults] = useState<PersonDetail[]>([]);
22 const [totalCount, setTotalCount] = useState(0);
23 const [loading, setLoading] = useState(false);
24 const [hasSearched, setHasSearched] = useState(false);
25
26 // Filters state
27 const [filters, setFilters] = useState<SearchFilters>({ conflicts: [], class_years: [] });
28
29 // Load all people on mount and get filters
30 useEffect(() => {
31 async function initialize() {
32 try {
33 // Get filters
34 const filterData = await getSearchFilters();
35 setFilters(filterData);
36
37 // Load all people initially
38 const params = {
39 q: '',
40 class_year: '',
41 conflict: '',
42 date_from: '',
43 date_to: '',
44 no_date: false,
45 has_document: false
46 };
47
48 const data = await searchPeople(params);
49 setResults(data.results);
50 setTotalCount(data.count);
51 setHasSearched(true);
52 } catch (err) {
53 console.error('Failed to initialize:', err);
54 }
55 }
56
57 initialize();
58 }, []);
59
60 const performSearch = async () => {
61 setLoading(true);
62 setHasSearched(true);
63
64 try {
65 const params = {
66 q: searchTerm,
67 class_year: selectedClassYears.join(','),
68 conflict: selectedConflicts.join(','),
69 date_from: dateFrom,
70 date_to: dateTo,
71 no_date: noDate,
72 has_document: hasDocument
73 };
74
75 const data = await searchPeople(params);
76 setResults(data.results);
77 setTotalCount(data.count);
78 } catch (err) {
79 console.error('Search failed:', err);
80 setResults([]);
81 setTotalCount(0);
82 } finally {
83 setLoading(false);
84 }
85 };
86
87 const handleSubmit = (e: React.FormEvent) => {
88 e.preventDefault();
89 performSearch();
90 };
91
92 const toggleClassYear = (year: number) => {
93 setSelectedClassYears(prev =>
94 prev.includes(year)
95 ? prev.filter(y => y !== year)
96 : [...prev, year]
97 );
98 };
99
100 const toggleConflict = (conflictId: number) => {
101 setSelectedConflicts(prev =>
102 prev.includes(conflictId)
103 ? prev.filter(c => c !== conflictId)
104 : [...prev, conflictId]
105 );
106 };
107
108 const clearFilters = () => {
109 setSearchTerm('');
110 setSelectedClassYears([]);
111 setSelectedConflicts([]);
112 setDateFrom('');
113 setDateTo('');
114 setNoDate(false);
115 setHasDocument(false);
116 };
117
118 return (
119 <div className="min-h-screen bg-vmi-cream">
120 <Header
121 breadcrumbs={[
122 { label: 'Home', href: '/' },
123 { label: 'Memorial Index', href: '/memorial' },
124 { label: 'Search Memorial' }
125 ]}
126 showSearch={false}
127 />
128
129 {/* Main Content */}
130 <main className="max-w-7xl mx-auto px-4 py-12">
131 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
132 {/* Search Form - Left Sidebar */}
133 <div className="lg:col-span-1">
134 <div className="bg-white border-2 border-gray-300 rounded-lg p-6 shadow-xl sticky top-6">
135 <h2 className="text-2xl font-bold text-vmi-red mb-6">Search Filters</h2>
136
137 <form onSubmit={handleSubmit} className="space-y-6">
138 {/* Memorial Document Filter */}
139 <div>
140 <label className="flex items-center cursor-pointer hover:bg-gray-50 p-2 rounded">
141 <input
142 type="checkbox"
143 checked={hasDocument}
144 onChange={(e) => setHasDocument(e.target.checked)}
145 className="mr-3 text-vmi-red focus:ring-vmi-gold w-4 h-4"
146 />
147 <span className="text-sm font-bold text-gray-700">Memorial Document Available</span>
148 </label>
149 </div>
150
151 {/* Name Search */}
152 <div>
153 <label htmlFor="search" className="block text-sm font-bold text-gray-700 mb-2">
154 Search by Name
155 </label>
156 <input
157 type="text"
158 id="search"
159 value={searchTerm}
160 onChange={(e) => setSearchTerm(e.target.value)}
161 placeholder="Enter name..."
162 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold"
163 />
164 </div>
165
166 {/* Class Year Filter */}
167 <div>
168 <label className="block text-sm font-bold text-gray-700 mb-2">
169 Class Year
170 </label>
171 <div className="max-h-40 overflow-y-auto border border-gray-300 rounded p-2">
172 {filters.class_years.map(year => (
173 <label key={year} className="flex items-center mb-1 cursor-pointer hover:bg-gray-50">
174 <input
175 type="checkbox"
176 checked={selectedClassYears.includes(year)}
177 onChange={() => toggleClassYear(year)}
178 className="mr-2 text-vmi-red focus:ring-vmi-gold"
179 />
180 <span className="text-sm">{year}</span>
181 </label>
182 ))}
183 </div>
184 </div>
185
186 {/* Conflict Filter */}
187 <div>
188 <label className="block text-sm font-bold text-gray-700 mb-2">
189 Conflict
190 </label>
191 <div className="max-h-40 overflow-y-auto border border-gray-300 rounded p-2">
192 {filters.conflicts.map(conflict => (
193 <label key={conflict.id} className="flex items-center mb-1 cursor-pointer hover:bg-gray-50">
194 <input
195 type="checkbox"
196 checked={selectedConflicts.includes(conflict.id)}
197 onChange={() => toggleConflict(conflict.id)}
198 className="mr-2 text-vmi-red focus:ring-vmi-gold"
199 />
200 <span className="text-sm">{conflict.name}</span>
201 </label>
202 ))}
203 </div>
204 </div>
205
206 {/* Date Range */}
207 <div>
208 <label className="block text-sm font-bold text-gray-700 mb-2">
209 Date of Death
210 </label>
211 <div className="space-y-2">
212 <input
213 type="date"
214 value={dateFrom}
215 onChange={(e) => setDateFrom(e.target.value)}
216 disabled={noDate}
217 placeholder="From"
218 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold disabled:bg-gray-100"
219 />
220 <input
221 type="date"
222 value={dateTo}
223 onChange={(e) => setDateTo(e.target.value)}
224 disabled={noDate}
225 placeholder="To"
226 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold disabled:bg-gray-100"
227 />
228 <label className="flex items-center cursor-pointer">
229 <input
230 type="checkbox"
231 checked={noDate}
232 onChange={(e) => setNoDate(e.target.checked)}
233 className="mr-2 text-vmi-red focus:ring-vmi-gold"
234 />
235 <span className="text-sm">No date recorded</span>
236 </label>
237 </div>
238 </div>
239
240 {/* Buttons */}
241 <div className="space-y-2">
242 <button
243 type="submit"
244 className="w-full bg-vmi-red text-white px-4 py-2 rounded font-bold hover:bg-vmi-dark-red transition-colors"
245 >
246 Search
247 </button>
248 <button
249 type="button"
250 onClick={clearFilters}
251 className="w-full bg-gray-300 text-gray-700 px-4 py-2 rounded font-bold hover:bg-gray-400 transition-colors"
252 >
253 Clear Filters
254 </button>
255 </div>
256 </form>
257 </div>
258 </div>
259
260 {/* Results - Right Side */}
261 <div className="lg:col-span-3">
262 <div className="bg-white border-2 border-gray-300 rounded-lg p-6 shadow-xl">
263 <div className="mb-6">
264 <h1 className="text-3xl font-bold text-vmi-red mb-2">Memorial Search</h1>
265 {hasSearched && (
266 <p className="text-gray-600">
267 Found {totalCount} {totalCount === 1 ? 'person' : 'people'}
268 </p>
269 )}
270 </div>
271
272 {loading ? (
273 <div className="text-center py-12">
274 <p className="text-gray-600">Searching...</p>
275 </div>
276 ) : results.length > 0 ? (
277 <div className="space-y-4">
278 {results.map((person) => (
279 <Link
280 key={person.id}
281 href={`/memorial/person/${person.id}`}
282 className="block p-6 border-2 border-gray-200 rounded-lg hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group"
283 >
284 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
285 <div>
286 <h3 className="text-xl font-bold text-gray-800 group-hover:text-vmi-red transition-colors flex items-center gap-2">
287 {(() => {
288 const name = person.full_display_name || person.display_name;
289 if (person.rank && person.full_display_name) {
290 return name.replace(person.rank + ' ', '').replace(person.rank + ', ', '');
291 }
292 return person.full_display_name ? name : person.display_name;
293 })()}
294 {person.has_awards && <AwardIcon className="flex-shrink-0" />}
295 {person.pdf_key && <DocumentIcon className="flex-shrink-0" />}
296 </h3>
297 {person.rank && (
298 <p className="text-gray-700">{person.rank}</p>
299 )}
300 {person.unit && (
301 <p className="text-gray-600 text-sm italic">{person.unit}</p>
302 )}
303 </div>
304 <div>
305 <p className="text-gray-700 font-semibold">{person.conflict_name}</p>
306 {/* THIS IS WHERE death_date_display IS USED in search results */}
307 {person.death_date_display && (
308 <p className="text-gray-600 text-sm">
309 {person.death_date_display}
310 </p>
311 )}
312 </div>
313 <div className="text-right">
314 <span className="text-vmi-red group-hover:text-vmi-dark-red font-bold">
315 View Details
316 </span>
317 </div>
318 </div>
319 </Link>
320 ))}
321 </div>
322 ) : hasSearched ? (
323 <div className="text-center py-12">
324 <p className="text-gray-600 text-lg">No results found matching your criteria.</p>
325 <button
326 onClick={clearFilters}
327 className="mt-4 text-vmi-red hover:text-vmi-dark-red underline font-semibold"
328 >
329 Clear filters and try again
330 </button>
331 </div>
332 ) : (
333 <div className="text-center py-12">
334 <p className="text-gray-600 text-lg">Use the filters to search the memorial database.</p>
335 </div>
336 )}
337 </div>
338 </div>
339 </div>
340 </main>
341 </div>
342 );
343 }