vmi-virtual-memorial/vmi-wd-frontend / 91f48cd

Browse files

search, index

Authored by espadonne
SHA
91f48cdcda7255b9edd8ff6202cd940ff497a428
Parents
de045ba
Tree
665a8e3

8 changed files

StatusFile+-
M app/memorial/conflict/[id]/page.tsx 8 13
A app/memorial/page.tsx 208 0
M app/memorial/person/[id]/page.tsx 14 20
A app/memorial/search/page.tsx 319 0
M app/page.tsx 26 12
A components/Header.tsx 65 0
M lib/api.ts 69 5
M tailwind.config.ts 14 3
app/memorial/conflict/[id]/page.tsxmodified
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
44
 import Link from 'next/link';
55
 import { useParams } from 'next/navigation';
66
 import { getConflicts, getPeopleByConflict, Conflict, Person } from '@/lib/api';
7
+import Header from '@/components/Header';
78
 
89
 export default function ConflictPage() {
910
   const params = useParams();
@@ -61,18 +62,12 @@ export default function ConflictPage() {
6162
 
6263
   return (
6364
     <div className="min-h-screen bg-vmi-cream">
64
-      {/* Header */}
65
-      <header className="bg-vmi-red shadow-lg">
66
-        <div className="max-w-6xl mx-auto px-4 py-6">
67
-          <nav className="flex items-center space-x-3 text-white">
68
-            <Link href="/" className="text-vmi-gold hover:text-white transition-colors">
69
-              Home
70
-            </Link>
71
-            <span className="text-vmi-gold">›</span>
72
-            <span className="font-semibold">{conflict.name}</span>
73
-          </nav>
74
-        </div>
75
-      </header>
65
+      <Header 
66
+        breadcrumbs={[
67
+          { label: 'Home', href: '/' },
68
+          { label: conflict.name }
69
+        ]}
70
+      />
7671
 
7772
       {/* Main Content */}
7873
       <main className="max-w-6xl mx-auto px-4 py-12">
@@ -111,7 +106,7 @@ export default function ConflictPage() {
111106
                   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"
112107
                 >
113108
                   <h3 className="text-xl font-bold text-gray-800 group-hover:text-vmi-red transition-colors mb-2">
114
-                    {person.display_name}
109
+                    {person.full_display_name || person.display_name}
115110
                   </h3>
116111
                   {person.rank && (
117112
                     <p className="text-gray-700 font-semibold">{person.rank}</p>
app/memorial/page.tsxadded
@@ -0,0 +1,208 @@
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
+}
app/memorial/person/[id]/page.tsxmodified
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
44
 import Link from 'next/link';
55
 import { useParams } from 'next/navigation';
66
 import { getPersonDetail, PersonDetail } from '@/lib/api';
7
+import Header from '@/components/Header';
78
 
89
 export default function PersonPage() {
910
   const params = useParams();
@@ -55,36 +56,29 @@ export default function PersonPage() {
5556
 
5657
   return (
5758
     <div className="min-h-screen bg-vmi-cream">
58
-      {/* Header */}
59
-      <header className="bg-vmi-red shadow-lg">
60
-        <div className="max-w-6xl mx-auto px-4 py-6">
61
-          <nav className="flex items-center space-x-3 text-white flex-wrap">
62
-            <Link href="/" className="text-vmi-gold hover:text-white transition-colors">
63
-              Home
64
-            </Link>
65
-            <span className="text-vmi-gold">›</span>
66
-            <Link 
67
-              href={`/memorial/conflict/${person.conflict}`}
68
-              className="text-vmi-gold hover:text-white transition-colors"
69
-            >
70
-              {person.conflict_name}
71
-            </Link>
72
-            <span className="text-vmi-gold">›</span>
73
-            <span className="font-semibold">{person.display_name}</span>
74
-          </nav>
75
-        </div>
76
-      </header>
59
+      <Header 
60
+        breadcrumbs={[
61
+          { label: 'Home', href: '/' },
62
+          { label: person.conflict_name, href: `/memorial/conflict/${person.conflict}` },
63
+          { label: person.display_name }
64
+        ]}
65
+      />
7766
 
7867
       {/* Main Content */}
7968
       <main className="max-w-6xl mx-auto px-4 py-12">
8069
         {/* Person Header */}
8170
         <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
8271
           <h1 className="text-4xl font-black text-vmi-red mb-6">
83
-            {person.display_name}
72
+            {person.full_display_name || person.display_name}
8473
           </h1>
8574
           
8675
           <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-gray-800">
8776
             <div className="space-y-3">
77
+              {person.class_year && (
78
+                <p className="text-lg">
79
+                  <span className="font-bold text-gray-700">Class Year:</span> {person.class_year}
80
+                </p>
81
+              )}
8882
               {person.rank && (
8983
                 <p className="text-lg">
9084
                   <span className="font-bold text-gray-700">Rank:</span> {person.rank}
app/memorial/search/page.tsxadded
@@ -0,0 +1,319 @@
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import Link from 'next/link';
5
+import { searchPeople, getSearchFilters, PersonSearchResult, SearchFilters } from '@/lib/api';
6
+import Header from '@/components/Header';
7
+
8
+export default function MemorialSearchPage() {
9
+  // Search state
10
+  const [searchTerm, setSearchTerm] = useState('');
11
+  const [selectedClassYears, setSelectedClassYears] = useState<number[]>([]);
12
+  const [selectedConflicts, setSelectedConflicts] = useState<number[]>([]);
13
+  const [dateFrom, setDateFrom] = useState('');
14
+  const [dateTo, setDateTo] = useState('');
15
+  const [noDate, setNoDate] = useState(false);
16
+  
17
+  // Results state
18
+  const [results, setResults] = useState<PersonSearchResult[]>([]);
19
+  const [totalCount, setTotalCount] = useState(0);
20
+  const [loading, setLoading] = useState(false);
21
+  const [hasSearched, setHasSearched] = useState(false);
22
+  
23
+  // Filters state
24
+  const [filters, setFilters] = useState<SearchFilters>({ conflicts: [], class_years: [] });
25
+
26
+  // Load all people on mount and get filters
27
+  useEffect(() => {
28
+    async function initialize() {
29
+      try {
30
+        // Get filters
31
+        const filterData = await getSearchFilters();
32
+        setFilters(filterData);
33
+        
34
+        // Load all people initially
35
+        const params = {
36
+          q: '',
37
+          class_year: '',
38
+          conflict: '',
39
+          date_from: '',
40
+          date_to: '',
41
+          no_date: false
42
+        };
43
+        
44
+        const data = await searchPeople(params);
45
+        setResults(data.results);
46
+        setTotalCount(data.count);
47
+        setHasSearched(true);
48
+      } catch (err) {
49
+        console.error('Failed to initialize:', err);
50
+      }
51
+    }
52
+    
53
+    initialize();
54
+  }, []);
55
+
56
+  const performSearch = async () => {
57
+    setLoading(true);
58
+    setHasSearched(true);
59
+    
60
+    try {
61
+      const params = {
62
+        q: searchTerm,
63
+        class_year: selectedClassYears.join(','),
64
+        conflict: selectedConflicts.join(','),
65
+        date_from: dateFrom,
66
+        date_to: dateTo,
67
+        no_date: noDate
68
+      };
69
+      
70
+      const data = await searchPeople(params);
71
+      setResults(data.results);
72
+      setTotalCount(data.count);
73
+    } catch (err) {
74
+      console.error('Search failed:', err);
75
+      setResults([]);
76
+      setTotalCount(0);
77
+    } finally {
78
+      setLoading(false);
79
+    }
80
+  };
81
+
82
+  const handleSubmit = (e: React.FormEvent) => {
83
+    e.preventDefault();
84
+    performSearch();
85
+  };
86
+
87
+  const toggleClassYear = (year: number) => {
88
+    setSelectedClassYears(prev =>
89
+      prev.includes(year) 
90
+        ? prev.filter(y => y !== year)
91
+        : [...prev, year]
92
+    );
93
+  };
94
+
95
+  const toggleConflict = (conflictId: number) => {
96
+    setSelectedConflicts(prev =>
97
+      prev.includes(conflictId)
98
+        ? prev.filter(c => c !== conflictId)
99
+        : [...prev, conflictId]
100
+    );
101
+  };
102
+
103
+  const clearFilters = () => {
104
+    setSearchTerm('');
105
+    setSelectedClassYears([]);
106
+    setSelectedConflicts([]);
107
+    setDateFrom('');
108
+    setDateTo('');
109
+    setNoDate(false);
110
+  };
111
+
112
+  return (
113
+    <div className="min-h-screen bg-vmi-cream">
114
+      <Header 
115
+        breadcrumbs={[
116
+          { label: 'Home', href: '/' },
117
+          { label: 'Memorial Index', href: '/memorial' },
118
+          { label: 'Search Memorial' }
119
+        ]}
120
+        showSearch={false}
121
+      />
122
+
123
+      {/* Main Content */}
124
+      <main className="max-w-7xl mx-auto px-4 py-12">
125
+        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
126
+          {/* Search Form - Left Sidebar */}
127
+          <div className="lg:col-span-1">
128
+            <div className="bg-white border-2 border-gray-300 rounded-lg p-6 shadow-xl sticky top-6">
129
+              <h2 className="text-2xl font-bold text-vmi-red mb-6">Search Filters</h2>
130
+              
131
+              <form onSubmit={handleSubmit} className="space-y-6">
132
+                {/* Name Search */}
133
+                <div>
134
+                  <label htmlFor="search" className="block text-sm font-bold text-gray-700 mb-2">
135
+                    Search by Name
136
+                  </label>
137
+                  <input
138
+                    type="text"
139
+                    id="search"
140
+                    value={searchTerm}
141
+                    onChange={(e) => setSearchTerm(e.target.value)}
142
+                    placeholder="Enter name..."
143
+                    className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold"
144
+                  />
145
+                </div>
146
+
147
+                {/* Class Year Filter */}
148
+                <div>
149
+                  <label className="block text-sm font-bold text-gray-700 mb-2">
150
+                    Class Year
151
+                  </label>
152
+                  <div className="max-h-40 overflow-y-auto border border-gray-300 rounded p-2">
153
+                    {filters.class_years.map(year => (
154
+                      <label key={year} className="flex items-center mb-1 cursor-pointer hover:bg-gray-50">
155
+                        <input
156
+                          type="checkbox"
157
+                          checked={selectedClassYears.includes(year)}
158
+                          onChange={() => toggleClassYear(year)}
159
+                          className="mr-2 text-vmi-red focus:ring-vmi-gold"
160
+                        />
161
+                        <span className="text-sm">{year}</span>
162
+                      </label>
163
+                    ))}
164
+                  </div>
165
+                </div>
166
+
167
+                {/* Conflict Filter */}
168
+                <div>
169
+                  <label className="block text-sm font-bold text-gray-700 mb-2">
170
+                    Conflict
171
+                  </label>
172
+                  <div className="max-h-40 overflow-y-auto border border-gray-300 rounded p-2">
173
+                    {filters.conflicts.map(conflict => (
174
+                      <label key={conflict.id} className="flex items-center mb-1 cursor-pointer hover:bg-gray-50">
175
+                        <input
176
+                          type="checkbox"
177
+                          checked={selectedConflicts.includes(conflict.id)}
178
+                          onChange={() => toggleConflict(conflict.id)}
179
+                          className="mr-2 text-vmi-red focus:ring-vmi-gold"
180
+                        />
181
+                        <span className="text-sm">{conflict.name}</span>
182
+                      </label>
183
+                    ))}
184
+                  </div>
185
+                </div>
186
+
187
+                {/* Date Range */}
188
+                <div>
189
+                  <label className="block text-sm font-bold text-gray-700 mb-2">
190
+                    Date of Death
191
+                  </label>
192
+                  <div className="space-y-2">
193
+                    <input
194
+                      type="date"
195
+                      value={dateFrom}
196
+                      onChange={(e) => setDateFrom(e.target.value)}
197
+                      disabled={noDate}
198
+                      placeholder="From"
199
+                      className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold disabled:bg-gray-100"
200
+                    />
201
+                    <input
202
+                      type="date"
203
+                      value={dateTo}
204
+                      onChange={(e) => setDateTo(e.target.value)}
205
+                      disabled={noDate}
206
+                      placeholder="To"
207
+                      className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold disabled:bg-gray-100"
208
+                    />
209
+                    <label className="flex items-center cursor-pointer">
210
+                      <input
211
+                        type="checkbox"
212
+                        checked={noDate}
213
+                        onChange={(e) => setNoDate(e.target.checked)}
214
+                        className="mr-2 text-vmi-red focus:ring-vmi-gold"
215
+                      />
216
+                      <span className="text-sm">No date recorded</span>
217
+                    </label>
218
+                  </div>
219
+                </div>
220
+
221
+                {/* Buttons */}
222
+                <div className="space-y-2">
223
+                  <button
224
+                    type="submit"
225
+                    className="w-full bg-vmi-red text-white px-4 py-2 rounded font-bold hover:bg-vmi-dark-red transition-colors"
226
+                  >
227
+                    Search
228
+                  </button>
229
+                  <button
230
+                    type="button"
231
+                    onClick={clearFilters}
232
+                    className="w-full bg-gray-300 text-gray-700 px-4 py-2 rounded font-bold hover:bg-gray-400 transition-colors"
233
+                  >
234
+                    Clear Filters
235
+                  </button>
236
+                </div>
237
+              </form>
238
+            </div>
239
+          </div>
240
+
241
+          {/* Results - Right Side */}
242
+          <div className="lg:col-span-3">
243
+            <div className="bg-white border-2 border-gray-300 rounded-lg p-6 shadow-xl">
244
+              <div className="mb-6">
245
+                <h1 className="text-3xl font-bold text-vmi-red mb-2">Memorial Search</h1>
246
+                {hasSearched && (
247
+                  <p className="text-gray-600">
248
+                    Found {totalCount} {totalCount === 1 ? 'person' : 'people'}
249
+                  </p>
250
+                )}
251
+              </div>
252
+
253
+              {loading ? (
254
+                <div className="text-center py-12">
255
+                  <p className="text-gray-600">Searching...</p>
256
+                </div>
257
+              ) : results.length > 0 ? (
258
+                <div className="space-y-4">
259
+                  {results.map((person) => (
260
+                    <Link
261
+                      key={person.id}
262
+                      href={`/memorial/person/${person.id}`}
263
+                      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"
264
+                    >
265
+                      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
266
+                        <div>
267
+                          <h3 className="text-xl font-bold text-gray-800 group-hover:text-vmi-red transition-colors">
268
+                            {person.full_display_name}
269
+                          </h3>
270
+                          {person.rank && (
271
+                            <p className="text-gray-700">{person.rank}</p>
272
+                          )}
273
+                          {person.unit && (
274
+                            <p className="text-gray-600 text-sm italic">{person.unit}</p>
275
+                          )}
276
+                        </div>
277
+                        <div>
278
+                          <p className="text-gray-700 font-semibold">{person.conflict_name}</p>
279
+                          {person.date_of_death && (
280
+                            <p className="text-gray-600 text-sm">
281
+                              {new Date(person.date_of_death).toLocaleDateString('en-US', {
282
+                                year: 'numeric',
283
+                                month: 'long',
284
+                                day: 'numeric'
285
+                              })}
286
+                            </p>
287
+                          )}
288
+                        </div>
289
+                        <div className="text-right">
290
+                          <span className="text-vmi-red group-hover:text-vmi-dark-red font-bold">
291
+                            View Details →
292
+                          </span>
293
+                        </div>
294
+                      </div>
295
+                    </Link>
296
+                  ))}
297
+                </div>
298
+              ) : hasSearched ? (
299
+                <div className="text-center py-12">
300
+                  <p className="text-gray-600 text-lg">No results found matching your criteria.</p>
301
+                  <button
302
+                    onClick={clearFilters}
303
+                    className="mt-4 text-vmi-red hover:text-vmi-dark-red underline font-semibold"
304
+                  >
305
+                    Clear filters and try again
306
+                  </button>
307
+                </div>
308
+              ) : (
309
+                <div className="text-center py-12">
310
+                  <p className="text-gray-600 text-lg">Use the filters to search the memorial database.</p>
311
+                </div>
312
+              )}
313
+            </div>
314
+          </div>
315
+        </div>
316
+      </main>
317
+    </div>
318
+  );
319
+}
app/page.tsxmodified
@@ -29,20 +29,34 @@ export default function Home() {
2929
     <div className="min-h-screen bg-vmi-cream">
3030
       {/* Header */}
3131
       <header className="bg-vmi-red shadow-lg">
32
-        <div className="max-w-6xl mx-auto px-4 py-6 flex justify-between items-center">
33
-          <div className="flex items-center space-x-3">
34
-            {/* VMI Seal placeholder - replace with actual image */}
35
-            <div className="w-16 h-16 bg-vmi-gold rounded-full flex items-center justify-center text-vmi-red font-bold text-xl border-4 border-white">
36
-              VMI
37
-            </div>
38
-            <div className="text-white">
39
-              <div className="text-sm uppercase tracking-wide">Virginia Military Institute</div>
40
-              <div className="text-xs">Lexington, Virginia</div>
32
+        <div className="max-w-6xl mx-auto px-4 py-6">
33
+          <div className="flex justify-between items-center">
34
+            <div className="flex items-center space-x-3">
35
+              {/* VMI Seal placeholder - replace with actual image */}
36
+              <div className="w-16 h-16 bg-vmi-gold rounded-full flex items-center justify-center text-vmi-red font-bold text-xl border-4 border-white">
37
+                VMI
38
+              </div>
39
+              <div className="text-white">
40
+                <div className="text-sm uppercase tracking-wide">Virginia Military Institute</div>
41
+                <div className="text-xs">Lexington, Virginia</div>
42
+              </div>
4143
             </div>
44
+            {/* Navigation */}
45
+            <nav className="flex items-center space-x-4">
46
+              <Link 
47
+                href="/memorial/search" 
48
+                className="text-vmi-gold hover:text-white transition-colors font-semibold"
49
+              >
50
+                Search Memorial
51
+              </Link>
52
+              <Link 
53
+                href="/memorial" 
54
+                className="bg-vmi-gold text-vmi-red px-6 py-2 rounded font-bold hover:bg-white transition-colors shadow-md"
55
+              >
56
+                View Complete Index
57
+              </Link>
58
+            </nav>
4259
           </div>
43
-          <Link href="/memorial" className="text-vmi-gold hover:text-white transition-colors font-semibold">
44
-            Memorial Index
45
-          </Link>
4660
         </div>
4761
       </header>
4862
 
components/Header.tsxadded
@@ -0,0 +1,65 @@
1
+import Link from 'next/link';
2
+
3
+interface BreadcrumbItem {
4
+  label: string;
5
+  href?: string;
6
+}
7
+
8
+interface HeaderProps {
9
+  breadcrumbs: BreadcrumbItem[];
10
+  showSearch?: boolean;
11
+  showIndex?: boolean;
12
+}
13
+
14
+export default function Header({ breadcrumbs, showSearch = true, showIndex = true }: HeaderProps) {
15
+  return (
16
+    <header className="bg-[#AE122A] shadow-lg">
17
+      <div className="max-w-6xl mx-auto px-4 py-6">
18
+        <div className="flex justify-between items-center">
19
+          {/* Breadcrumb Navigation */}
20
+          <nav className="flex items-center space-x-3 text-white flex-wrap">
21
+            {breadcrumbs.map((item, index) => (
22
+              <div key={index} className="flex items-center">
23
+                {item.href ? (
24
+                  <Link 
25
+                    href={item.href} 
26
+                    className="text-[#FFD619] hover:text-white transition-colors"
27
+                  >
28
+                    {item.label}
29
+                  </Link>
30
+                ) : (
31
+                  <span className="font-semibold">{item.label}</span>
32
+                )}
33
+                {index < breadcrumbs.length - 1 && (
34
+                  <span className="text-[#FFD619] ml-3">›</span>
35
+                )}
36
+              </div>
37
+            ))}
38
+          </nav>
39
+
40
+          {/* Right Navigation */}
41
+          {(showSearch || showIndex) && (
42
+            <div className="flex items-center space-x-4">
43
+              {showSearch && (
44
+                <Link 
45
+                  href="/memorial/search" 
46
+                  className="text-[#FFD619] hover:text-white transition-colors font-semibold"
47
+                >
48
+                  Search Memorial
49
+                </Link>
50
+              )}
51
+              {showIndex && (
52
+                <Link 
53
+                  href="/memorial" 
54
+                  className="bg-[#FFD619] text-[#AE122A] px-6 py-2 rounded font-bold hover:bg-white transition-colors shadow-md"
55
+                >
56
+                  View Complete Index
57
+                </Link>
58
+              )}
59
+            </div>
60
+          )}
61
+        </div>
62
+      </div>
63
+    </header>
64
+  );
65
+}
lib/api.tsmodified
@@ -1,7 +1,7 @@
11
 // API configuration for the VMI Memorial frontend
22
 
3
-const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
4
-// const API_BASE_URL = 'https://web-production-6002.up.railway.app';
3
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
4
+// const API_BASE_URL = 'https://web-production-6002.up.railway.app/api';
55
 
66
 
77
 export interface Conflict {
@@ -19,6 +19,8 @@ export interface Person {
1919
   display_name: string;
2020
   rank: string;
2121
   unit: string;
22
+  class_year?: number;
23
+  full_display_name?: string;
2224
 }
2325
 
2426
 export interface PersonDetail extends Person {
@@ -33,9 +35,35 @@ export interface PersonDetail extends Person {
3335
   pdf_url: string | null;
3436
 }
3537
 
38
+export interface PersonSearchResult {
39
+  id: number;
40
+  display_name: string;
41
+  full_display_name: string;
42
+  class_year: number | null;
43
+  rank: string;
44
+  unit: string;
45
+  date_of_death: string | null;
46
+  conflict_name: string;
47
+  conflict_id: number;
48
+}
49
+
50
+export interface SearchFilters {
51
+  conflicts: Conflict[];
52
+  class_years: number[];
53
+}
54
+
55
+export interface SearchParams {
56
+  q?: string;
57
+  class_year?: string;
58
+  conflict?: string;
59
+  date_from?: string;
60
+  date_to?: string;
61
+  no_date?: boolean;
62
+}
63
+
3664
 // Fetch all conflicts
3765
 export async function getConflicts(): Promise<Conflict[]> {
38
-  const response = await fetch(`${API_BASE_URL}/api/memorial/conflicts/`);
66
+  const response = await fetch(`${API_BASE_URL}/memorial/conflicts/`);
3967
   if (!response.ok) {
4068
     throw new Error('Failed to fetch conflicts');
4169
   }
@@ -45,7 +73,7 @@ export async function getConflicts(): Promise<Conflict[]> {
4573
 
4674
 // Fetch people by conflict
4775
 export async function getPeopleByConflict(conflictId: number): Promise<Person[]> {
48
-  const response = await fetch(`${API_BASE_URL}/api/memorial/persons/?conflict=${conflictId}`);
76
+  const response = await fetch(`${API_BASE_URL}/memorial/persons/?conflict=${conflictId}`);
4977
   if (!response.ok) {
5078
     throw new Error('Failed to fetch people');
5179
   }
@@ -55,9 +83,45 @@ export async function getPeopleByConflict(conflictId: number): Promise<Person[]>
5583
 
5684
 // Fetch person details
5785
 export async function getPersonDetail(personId: number): Promise<PersonDetail> {
58
-  const response = await fetch(`${API_BASE_URL}/api/memorial/persons/${personId}/`);
86
+  const response = await fetch(`${API_BASE_URL}/memorial/persons/${personId}/`);
5987
   if (!response.ok) {
6088
     throw new Error('Failed to fetch person details');
6189
   }
6290
   return response.json();
91
+}
92
+
93
+// Fetch memorial index (all conflicts with casualties)
94
+export async function getMemorialIndex(): Promise<Conflict[]> {
95
+  const response = await fetch(`${API_BASE_URL}/memorial/index/`);
96
+  if (!response.ok) {
97
+    throw new Error('Failed to fetch memorial index');
98
+  }
99
+  return response.json();
100
+}
101
+
102
+// Search people with filters
103
+export async function searchPeople(params: SearchParams): Promise<{ count: number; results: PersonSearchResult[] }> {
104
+  const queryParams = new URLSearchParams();
105
+  
106
+  if (params.q) queryParams.append('q', params.q);
107
+  if (params.class_year) queryParams.append('class_year', params.class_year);
108
+  if (params.conflict) queryParams.append('conflict', params.conflict);
109
+  if (params.date_from) queryParams.append('date_from', params.date_from);
110
+  if (params.date_to) queryParams.append('date_to', params.date_to);
111
+  if (params.no_date !== undefined) queryParams.append('no_date', params.no_date.toString());
112
+  
113
+  const response = await fetch(`${API_BASE_URL}/memorial/persons/search/?${queryParams.toString()}`);
114
+  if (!response.ok) {
115
+    throw new Error('Failed to search people');
116
+  }
117
+  return response.json();
118
+}
119
+
120
+// Get available search filters
121
+export async function getSearchFilters(): Promise<SearchFilters> {
122
+  const response = await fetch(`${API_BASE_URL}/memorial/search-filters/`);
123
+  if (!response.ok) {
124
+    throw new Error('Failed to fetch search filters');
125
+  }
126
+  return response.json();
63127
 }
tailwind.config.tsmodified
@@ -9,11 +9,22 @@ const config: Config = {
99
   theme: {
1010
     extend: {
1111
       colors: {
12
-        background: "var(--background)",
13
-        foreground: "var(--foreground)",
12
+        'vmi-red': '#AE122A',
13
+        'vmi-gold': '#FFD619',
14
+        'vmi-dark-red': '#8A0E22',
15
+        'vmi-light-gold': '#FFF3B8',
16
+        'vmi-cream': '#d7d4c9',
17
+        'vmi-black': '#1A1A1A',
18
+        'vmi-gray': '#4A4A4A',
19
+        'vmi-light-gray': '#F5F5F5',
20
+      },
21
+      fontFamily: {
22
+        'serif': ['Crimson Text', 'Georgia', 'serif'],
23
+        'display': ['Playfair Display', 'Georgia', 'serif'],
1424
       },
1525
     },
1626
   },
1727
   plugins: [],
1828
 };
19
-export default config;
29
+
30
+export default config;