@@ -21,25 +21,28 @@ export default function MemorialIndexPage() { |
| 21 | const [loading, setLoading] = useState(true); | 21 | const [loading, setLoading] = useState(true); |
| 22 | const [error, setError] = useState<string | null>(null); | 22 | const [error, setError] = useState<string | null>(null); |
| 23 | const [expandedConflicts, setExpandedConflicts] = useState<Set<number>>(new Set()); | 23 | const [expandedConflicts, setExpandedConflicts] = useState<Set<number>>(new Set()); |
| | 24 | + const [sortBy, setSortBy] = useState<'alphabetical' | 'class_year'>('alphabetical'); |
| 24 | | 25 | |
| 25 | useEffect(() => { | 26 | useEffect(() => { |
| 26 | async function fetchData() { | 27 | async function fetchData() { |
| 27 | try { | 28 | try { |
| 28 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; | 29 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; |
| 29 | - const response = await fetch(`${apiUrl}/memorial/index/`); | 30 | + const response = await fetch(`${apiUrl}/memorial/index/?sort=${sortBy}`); |
| 30 | - | 31 | + |
| 31 | if (!response.ok) { | 32 | if (!response.ok) { |
| 32 | throw new Error('Failed to fetch memorial index'); | 33 | throw new Error('Failed to fetch memorial index'); |
| 33 | } | 34 | } |
| 34 | - | 35 | + |
| 35 | const data = await response.json(); | 36 | const data = await response.json(); |
| 36 | setConflicts(data); | 37 | setConflicts(data); |
| 37 | - | 38 | + |
| 38 | - // By default, expand conflicts with casualties | 39 | + // By default, expand conflicts with casualties (only on first load) |
| 39 | - const defaultExpanded = new Set<number>( | 40 | + if (expandedConflicts.size === 0) { |
| 40 | - data.filter((c: ConflictWithCasualties) => c.casualty_count > 0).map((c: ConflictWithCasualties) => c.id) | 41 | + const defaultExpanded = new Set<number>( |
| 41 | - ); | 42 | + data.filter((c: ConflictWithCasualties) => c.casualty_count > 0).map((c: ConflictWithCasualties) => c.id) |
| 42 | - setExpandedConflicts(defaultExpanded); | 43 | + ); |
| | 44 | + setExpandedConflicts(defaultExpanded); |
| | 45 | + } |
| 43 | } catch (err) { | 46 | } catch (err) { |
| 44 | setError(err instanceof Error ? err.message : 'Failed to load data'); | 47 | setError(err instanceof Error ? err.message : 'Failed to load data'); |
| 45 | console.error(err); | 48 | console.error(err); |
@@ -47,9 +50,9 @@ export default function MemorialIndexPage() { |
| 47 | setLoading(false); | 50 | setLoading(false); |
| 48 | } | 51 | } |
| 49 | } | 52 | } |
| 50 | - | 53 | + |
| 51 | fetchData(); | 54 | fetchData(); |
| 52 | - }, []); | 55 | + }, [sortBy]); |
| 53 | | 56 | |
| 54 | const toggleConflict = (conflictId: number) => { | 57 | const toggleConflict = (conflictId: number) => { |
| 55 | const newExpanded = new Set(expandedConflicts); | 58 | const newExpanded = new Set(expandedConflicts); |
@@ -123,13 +126,41 @@ export default function MemorialIndexPage() { |
| 123 | <span className="text-vmi-red font-bold">{totalCasualties}</span> | 126 | <span className="text-vmi-red font-bold">{totalCasualties}</span> |
| 124 | </div> | 127 | </div> |
| 125 | </div> | 128 | </div> |
| 126 | - <div className="mt-6"> | 129 | + <div className="mt-6 flex justify-between items-center"> |
| 127 | <button | 130 | <button |
| 128 | onClick={toggleAll} | 131 | onClick={toggleAll} |
| 129 | className="bg-vmi-red text-white px-6 py-2 rounded hover:bg-vmi-dark-red transition-colors font-semibold" | 132 | className="bg-vmi-red text-white px-6 py-2 rounded hover:bg-vmi-dark-red transition-colors font-semibold" |
| 130 | > | 133 | > |
| 131 | {expandedConflicts.size === conflicts.length ? 'Collapse All' : 'Expand All'} | 134 | {expandedConflicts.size === conflicts.length ? 'Collapse All' : 'Expand All'} |
| 132 | </button> | 135 | </button> |
| | 136 | + |
| | 137 | + {/* Sort Toggle */} |
| | 138 | + <div className="flex items-center gap-2"> |
| | 139 | + <span className="text-sm text-gray-600">Sort by:</span> |
| | 140 | + <button |
| | 141 | + onClick={() => setSortBy(sortBy === 'alphabetical' ? 'class_year' : 'alphabetical')} |
| | 142 | + className="flex items-center bg-white border-2 border-gray-300 rounded-full p-1 shadow-sm hover:shadow-md transition-shadow" |
| | 143 | + > |
| | 144 | + <span |
| | 145 | + className={`px-3 py-1 rounded-full transition-all ${ |
| | 146 | + sortBy === 'alphabetical' |
| | 147 | + ? 'bg-vmi-red text-white font-semibold' |
| | 148 | + : 'text-gray-500' |
| | 149 | + }`} |
| | 150 | + > |
| | 151 | + ABC |
| | 152 | + </span> |
| | 153 | + <span |
| | 154 | + className={`px-3 py-1 rounded-full transition-all ${ |
| | 155 | + sortBy === 'class_year' |
| | 156 | + ? 'bg-vmi-red text-white font-semibold' |
| | 157 | + : 'text-gray-500' |
| | 158 | + }`} |
| | 159 | + > |
| | 160 | + '42 |
| | 161 | + </span> |
| | 162 | + </button> |
| | 163 | + </div> |
| 133 | </div> | 164 | </div> |
| 134 | </div> | 165 | </div> |
| 135 | | 166 | |
@@ -179,6 +210,9 @@ export default function MemorialIndexPage() { |
| 179 | {person.rank | 210 | {person.rank |
| 180 | ? person.display_name.replace(person.rank + ' ', '').replace(person.rank + ', ', '') | 211 | ? person.display_name.replace(person.rank + ' ', '').replace(person.rank + ', ', '') |
| 181 | : person.display_name} | 212 | : person.display_name} |
| | 213 | + {person.class_year && ( |
| | 214 | + <span className="text-gray-600 font-normal">'{String(person.class_year).slice(-2)}</span> |
| | 215 | + )} |
| 182 | {person.pdf_key && <DocumentIcon className="flex-shrink-0" />} | 216 | {person.pdf_key && <DocumentIcon className="flex-shrink-0" />} |
| 183 | </h3> | 217 | </h3> |
| 184 | {person.rank && ( | 218 | {person.rank && ( |