vmi-virtual-memorial/vmi-wd-frontend / 15fb122

Browse files

update for awards

Authored by espadonne
SHA
15fb122bcf939b677f78f41b748b004cc29b263c
Parents
44c9250
Tree
32678dc

21 changed files

StatusFile+-
A app/awards/[id]/page.tsx 193 0
A app/awards/page.tsx 124 0
M app/memorial/conflict/[id]/page.tsx 2 0
M app/memorial/page.tsx 2 0
M app/memorial/person/[id]/page.tsx 61 0
M app/memorial/search/page.tsx 2 0
M app/page.tsx 10 4
A components/AwardIcon.tsx 22 0
M components/Header.tsx 15 6
M lib/api.ts 62 2
A public/AirForceCross.jpg bin
A public/CroixDeGuerre.jpg bin
A public/DistinguishedServiceCross.jpg bin
A public/DistinguishedServiceOrder.png bin
A public/MedailleMilitaire.jpg bin
A public/MoHAirForce.jpg bin
A public/MoHArmy.jpg bin
A public/MoHNavy.jpg bin
A public/NavyCross.jpg bin
A public/SilverStar.png bin
A public/VictoriaCross.jpg bin
app/awards/[id]/page.tsxadded
@@ -0,0 +1,193 @@
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import { useParams } from 'next/navigation';
5
+import Link from 'next/link';
6
+import Image from 'next/image';
7
+import Header from '@/components/Header';
8
+import DocumentIcon from '@/components/DocumentIcon';
9
+import { getAwardDetail, AwardDetail, AwardRecipient } from '@/lib/api';
10
+
11
+export default function AwardDetailPage() {
12
+  const params = useParams();
13
+  const awardId = Number(params.id);
14
+
15
+  const [award, setAward] = useState<AwardDetail | null>(null);
16
+  const [loading, setLoading] = useState(true);
17
+  const [error, setError] = useState<string | null>(null);
18
+
19
+  useEffect(() => {
20
+    async function fetchAward() {
21
+      try {
22
+        const data = await getAwardDetail(awardId);
23
+        setAward(data);
24
+      } catch (err) {
25
+        setError('Failed to load award details');
26
+        console.error(err);
27
+      } finally {
28
+        setLoading(false);
29
+      }
30
+    }
31
+    if (awardId) {
32
+      fetchAward();
33
+    }
34
+  }, [awardId]);
35
+
36
+  // Helper to get image path
37
+  const getImagePath = (filename: string) => {
38
+    const extensions = ['.jpg', '.png', '.jpeg', '.gif'];
39
+    for (const ext of extensions) {
40
+      if (filename.toLowerCase().endsWith(ext)) {
41
+        return `/${filename}`;
42
+      }
43
+    }
44
+    return `/${filename}.jpg`;
45
+  };
46
+
47
+  const breadcrumbs = [
48
+    { label: 'Home', href: '/' },
49
+    { label: 'Awards', href: '/awards' },
50
+    { label: award?.name || 'Loading...' }
51
+  ];
52
+
53
+  if (loading) {
54
+    return (
55
+      <div className="min-h-screen bg-vmi-cream">
56
+        <Header breadcrumbs={breadcrumbs} showAwards={false} />
57
+        <main className="max-w-6xl mx-auto px-4 py-12">
58
+          <p className="text-center text-gray-600">Loading award details...</p>
59
+        </main>
60
+      </div>
61
+    );
62
+  }
63
+
64
+  if (error || !award) {
65
+    return (
66
+      <div className="min-h-screen bg-vmi-cream">
67
+        <Header breadcrumbs={breadcrumbs} showAwards={false} />
68
+        <main className="max-w-6xl mx-auto px-4 py-12">
69
+          <p className="text-center text-red-600">{error || 'Award not found'}</p>
70
+          <div className="text-center mt-4">
71
+            <Link href="/awards" className="text-vmi-red hover:underline">
72
+              ← Back to Awards
73
+            </Link>
74
+          </div>
75
+        </main>
76
+      </div>
77
+    );
78
+  }
79
+
80
+  return (
81
+    <div className="min-h-screen bg-vmi-cream">
82
+      <Header breadcrumbs={breadcrumbs} showAwards={false} />
83
+
84
+      <main className="max-w-6xl mx-auto px-4 py-12">
85
+        {/* Award Header Section */}
86
+        <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-8 shadow-xl">
87
+          <div className="flex flex-col md:flex-row items-center gap-8">
88
+            {/* Award Image */}
89
+            <div className="relative w-32 h-44 flex-shrink-0">
90
+              <Image
91
+                src={getImagePath(award.image_filename)}
92
+                alt={award.name}
93
+                fill
94
+                className="object-contain"
95
+                sizes="128px"
96
+                priority
97
+              />
98
+            </div>
99
+
100
+            {/* Award Info */}
101
+            <div className="text-center md:text-left">
102
+              <h1 className="text-3xl md:text-4xl font-black mb-4 text-vmi-red">
103
+                {award.name}
104
+              </h1>
105
+              <p className="text-gray-700 text-lg mb-4">
106
+                {award.short_description}
107
+              </p>
108
+              <p className="text-vmi-red font-bold text-xl">
109
+                {award.recipient_count} VMI {award.recipient_count === 1 ? 'Recipient' : 'Recipients'}
110
+                {award.total_awards_given > award.recipient_count && (
111
+                  <span className="text-gray-600 font-normal text-base ml-2">
112
+                    ({award.total_awards_given} total awards)
113
+                  </span>
114
+                )}
115
+              </p>
116
+            </div>
117
+          </div>
118
+        </div>
119
+
120
+        {/* Long Description Section */}
121
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-8 shadow-xl">
122
+          <h2 className="text-2xl font-bold mb-4 text-vmi-red">About This Award</h2>
123
+          <div className="prose max-w-none text-gray-700 whitespace-pre-line">
124
+            {award.long_description}
125
+          </div>
126
+        </div>
127
+
128
+        {/* Recipients Section */}
129
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
130
+          <h2 className="text-2xl font-bold mb-6 text-vmi-red">
131
+            VMI Alumni Recipients
132
+          </h2>
133
+
134
+          {award.recipients.length === 0 ? (
135
+            <p className="text-center text-gray-600">
136
+              No VMI recipients have been added yet.
137
+            </p>
138
+          ) : (
139
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
140
+              {award.recipients.map((recipient: AwardRecipient) => (
141
+                <Link
142
+                  key={`${recipient.person_id}-${recipient.count}`}
143
+                  href={`/memorial/person/${recipient.person_id}`}
144
+                  className="block p-4 rounded-lg border-2 border-gray-200 hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group"
145
+                >
146
+                  <div className="flex items-start justify-between">
147
+                    <div className="flex-1">
148
+                      <h3 className="font-bold text-gray-800 group-hover:text-vmi-red transition-colors">
149
+                        {recipient.display_name}
150
+                        {recipient.count > 1 && (
151
+                          <span className="ml-2 text-sm text-vmi-gold font-normal">
152
+                            (×{recipient.count})
153
+                          </span>
154
+                        )}
155
+                      </h3>
156
+                      {recipient.class_year && (
157
+                        <p className="text-sm text-gray-600">
158
+                          Class of {recipient.class_year}
159
+                          {recipient.class_letter && ` (${recipient.class_letter})`}
160
+                        </p>
161
+                      )}
162
+                      <p className="text-sm text-gray-500">{recipient.conflict_name}</p>
163
+                      {recipient.date_awarded && (
164
+                        <p className="text-xs text-gray-400 mt-1">
165
+                          Awarded: {new Date(recipient.date_awarded).toLocaleDateString()}
166
+                        </p>
167
+                      )}
168
+                    </div>
169
+                    {recipient.pdf_key && (
170
+                      <DocumentIcon className="ml-2 flex-shrink-0" />
171
+                    )}
172
+                  </div>
173
+                  {recipient.citation && (
174
+                    <p className="mt-2 text-sm text-gray-600 line-clamp-2 italic">
175
+                      "{recipient.citation}"
176
+                    </p>
177
+                  )}
178
+                </Link>
179
+              ))}
180
+            </div>
181
+          )}
182
+        </div>
183
+
184
+        {/* Back Link */}
185
+        <div className="mt-8 text-center">
186
+          <Link href="/awards" className="text-vmi-red hover:underline font-semibold">
187
+            ← Back to All Awards
188
+          </Link>
189
+        </div>
190
+      </main>
191
+    </div>
192
+  );
193
+}
app/awards/page.tsxadded
@@ -0,0 +1,124 @@
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import Link from 'next/link';
5
+import Image from 'next/image';
6
+import Header from '@/components/Header';
7
+import { getAwards, Award } from '@/lib/api';
8
+
9
+export default function AwardsPage() {
10
+  const [awards, setAwards] = useState<Award[]>([]);
11
+  const [loading, setLoading] = useState(true);
12
+  const [error, setError] = useState<string | null>(null);
13
+
14
+  useEffect(() => {
15
+    async function fetchAwards() {
16
+      try {
17
+        const data = await getAwards();
18
+        setAwards(data);
19
+      } catch (err) {
20
+        setError('Failed to load awards');
21
+        console.error(err);
22
+      } finally {
23
+        setLoading(false);
24
+      }
25
+    }
26
+    fetchAwards();
27
+  }, []);
28
+
29
+  const breadcrumbs = [
30
+    { label: 'Home', href: '/' },
31
+    { label: 'Awards for Heroism and Gallantry' }
32
+  ];
33
+
34
+  // Helper to get image extension
35
+  const getImagePath = (filename: string) => {
36
+    // Check for common extensions in public folder
37
+    const extensions = ['.jpg', '.png', '.jpeg', '.gif'];
38
+    for (const ext of extensions) {
39
+      // The filename in the DB might already include extension or not
40
+      if (filename.toLowerCase().endsWith(ext)) {
41
+        return `/${filename}`;
42
+      }
43
+    }
44
+    // Default to .jpg if no extension
45
+    return `/${filename}.jpg`;
46
+  };
47
+
48
+  return (
49
+    <div className="min-h-screen bg-vmi-cream">
50
+      <Header breadcrumbs={breadcrumbs} showAwards={false} />
51
+
52
+      <main className="max-w-6xl mx-auto px-4 py-12">
53
+        {/* Title Section */}
54
+        <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
55
+          <h1 className="text-4xl font-black text-center mb-4 text-vmi-red">
56
+            Awards for Heroism and Gallantry
57
+          </h1>
58
+          <p className="text-center text-gray-700 max-w-3xl mx-auto">
59
+            VMI alumni have been recognized with the nation&apos;s highest military decorations for valor
60
+            and heroism in combat. These awards honor extraordinary acts of bravery and selfless service
61
+            in defense of our country.
62
+          </p>
63
+        </div>
64
+
65
+        {/* Awards Grid */}
66
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
67
+          <h2 className="text-3xl font-bold mb-8 text-center text-vmi-red">
68
+            Military Decorations
69
+          </h2>
70
+
71
+          {loading && (
72
+            <p className="text-center text-gray-600">Loading awards...</p>
73
+          )}
74
+
75
+          {error && (
76
+            <p className="text-center text-red-600">{error}</p>
77
+          )}
78
+
79
+          {!loading && !error && awards.length === 0 && (
80
+            <p className="text-center text-gray-600">
81
+              No awards found. Please add some through the admin panel.
82
+            </p>
83
+          )}
84
+
85
+          {!loading && !error && awards.length > 0 && (
86
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
87
+              {awards.map((award) => (
88
+                <Link
89
+                  key={award.id}
90
+                  href={`/awards/${award.id}`}
91
+                  className="block p-6 rounded-lg border-2 border-gray-200 hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group"
92
+                >
93
+                  <div className="flex flex-col items-center text-center">
94
+                    {/* Award Image */}
95
+                    <div className="relative w-24 h-32 mb-4">
96
+                      <Image
97
+                        src={getImagePath(award.image_filename)}
98
+                        alt={award.name}
99
+                        fill
100
+                        className="object-contain"
101
+                        sizes="96px"
102
+                      />
103
+                    </div>
104
+
105
+                    {/* Award Info */}
106
+                    <h3 className="text-lg font-bold text-gray-800 group-hover:text-vmi-red transition-colors mb-2">
107
+                      {award.name}
108
+                    </h3>
109
+                    <p className="text-gray-600 text-sm line-clamp-2 mb-3">
110
+                      {award.short_description}
111
+                    </p>
112
+                    <p className="text-vmi-red font-bold">
113
+                      {award.recipient_count} VMI {award.recipient_count === 1 ? 'Recipient' : 'Recipients'}
114
+                    </p>
115
+                  </div>
116
+                </Link>
117
+              ))}
118
+            </div>
119
+          )}
120
+        </div>
121
+      </main>
122
+    </div>
123
+  );
124
+}
app/memorial/conflict/[id]/page.tsxmodified
@@ -6,6 +6,7 @@ import { useParams } from 'next/navigation';
66
 import { getConflicts, getPeopleByConflict, Conflict, PersonDetail } from '@/lib/api';
77
 import Header from '@/components/Header';
88
 import DocumentIcon from '@/components/DocumentIcon';
9
+import AwardIcon from '@/components/AwardIcon';
910
 import Pagination from '@/components/Pagination';
1011
 
1112
 export default function ConflictPage() {
@@ -152,6 +153,7 @@ useEffect(() => {
152153
                         }
153154
                         return name;
154155
                       })()}
156
+                      {person.has_awards && <AwardIcon className="flex-shrink-0" />}
155157
                       {person.pdf_key && <DocumentIcon className="flex-shrink-0" />}
156158
                     </h3>
157159
                     {person.rank && (
app/memorial/page.tsxmodified
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
44
 import Link from 'next/link';
55
 import Header from '@/components/Header';
66
 import DocumentIcon from '@/components/DocumentIcon';
7
+import AwardIcon from '@/components/AwardIcon';
78
 import { PersonDetail } from '@/lib/api';
89
 
910
 interface ConflictWithCasualties {
@@ -215,6 +216,7 @@ export default function MemorialIndexPage() {
215216
                           {person.class_year && (
216217
                             <span className="text-gray-600 font-normal">&apos;{String(person.class_year).slice(-2)}</span>
217218
                           )}
219
+                          {person.has_awards && <AwardIcon className="flex-shrink-0" />}
218220
                           {person.pdf_key && <DocumentIcon className="flex-shrink-0" />}
219221
                         </h3>
220222
                         {person.rank && (
app/memorial/person/[id]/page.tsxmodified
@@ -2,6 +2,7 @@
22
 
33
 import { useState, useEffect } from 'react';
44
 import Link from 'next/link';
5
+import Image from 'next/image';
56
 import { useParams } from 'next/navigation';
67
 import { getPersonDetail, PersonDetailWithContributions } from '@/lib/api';
78
 import Header from '@/components/Header';
@@ -169,6 +170,66 @@ export default function PersonPage() {
169170
           </div>
170171
         )}
171172
 
173
+        {/* Awards Section */}
174
+        {person.awards && person.awards.length > 0 && (
175
+          <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-12 shadow-xl">
176
+            <h2 className="text-2xl font-bold mb-6 text-vmi-red">
177
+              Awards for Heroism &amp; Gallantry
178
+            </h2>
179
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
180
+              {person.awards.map((award) => {
181
+                const getImagePath = (filename: string) => {
182
+                  const extensions = ['.jpg', '.png', '.jpeg', '.gif'];
183
+                  for (const ext of extensions) {
184
+                    if (filename.toLowerCase().endsWith(ext)) {
185
+                      return `/${filename}`;
186
+                    }
187
+                  }
188
+                  return `/${filename}.jpg`;
189
+                };
190
+
191
+                return (
192
+                  <Link
193
+                    key={award.award_id}
194
+                    href={`/awards/${award.award_id}`}
195
+                    className="flex items-center gap-4 p-4 rounded-lg border-2 border-gray-200 hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group"
196
+                  >
197
+                    <div className="relative w-16 h-20 flex-shrink-0">
198
+                      <Image
199
+                        src={getImagePath(award.award_image_filename)}
200
+                        alt={award.award_name}
201
+                        fill
202
+                        className="object-contain"
203
+                        sizes="64px"
204
+                      />
205
+                    </div>
206
+                    <div>
207
+                      <h3 className="font-bold text-gray-800 group-hover:text-vmi-red transition-colors">
208
+                        {award.award_name}
209
+                        {award.count > 1 && (
210
+                          <span className="ml-2 text-sm text-vmi-gold">
211
+                            (×{award.count})
212
+                          </span>
213
+                        )}
214
+                      </h3>
215
+                      {award.date_awarded && (
216
+                        <p className="text-sm text-gray-500">
217
+                          {new Date(award.date_awarded).toLocaleDateString()}
218
+                        </p>
219
+                      )}
220
+                      {award.citation && (
221
+                        <p className="text-sm text-gray-600 italic line-clamp-2 mt-1">
222
+                          "{award.citation}"
223
+                        </p>
224
+                      )}
225
+                    </div>
226
+                  </Link>
227
+                );
228
+              })}
229
+            </div>
230
+          </div>
231
+        )}
232
+
172233
         {/* PDF Viewer */}
173234
         <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mb-12">
174235
           <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
app/memorial/search/page.tsxmodified
@@ -5,6 +5,7 @@ import Link from 'next/link';
55
 import { searchPeople, getSearchFilters, PersonDetail, SearchFilters } from '@/lib/api';
66
 import Header from '@/components/Header';
77
 import DocumentIcon from '@/components/DocumentIcon';
8
+import AwardIcon from '@/components/AwardIcon';
89
 
910
 export default function MemorialSearchPage() {
1011
   // Search state
@@ -290,6 +291,7 @@ export default function MemorialSearchPage() {
290291
                               }
291292
                               return person.full_display_name ? name : person.display_name;
292293
                             })()}
294
+                            {person.has_awards && <AwardIcon className="flex-shrink-0" />}
293295
                             {person.pdf_key && <DocumentIcon className="flex-shrink-0" />}
294296
                           </h3>
295297
                           {person.rank && (
app/page.tsxmodified
@@ -43,14 +43,20 @@ export default function Home() {
4343
             </div>
4444
             {/* Navigation */}
4545
             <nav className="flex items-center space-x-4">
46
-              <Link 
47
-                href="/memorial/search" 
46
+              <Link
47
+                href="/memorial/search"
4848
                 className="text-vmi-gold hover:text-white transition-colors font-semibold"
4949
               >
5050
                 Search Memorial
5151
               </Link>
52
-              <Link 
53
-                href="/memorial" 
52
+              <Link
53
+                href="/awards"
54
+                className="bg-vmi-gold text-vmi-red px-4 py-2 rounded font-bold hover:bg-white transition-colors shadow-md text-center text-sm leading-tight"
55
+              >
56
+                Awards for<br />Heroism &amp; Gallantry
57
+              </Link>
58
+              <Link
59
+                href="/memorial"
5460
                 className="bg-vmi-gold text-vmi-red px-6 py-2 rounded font-bold hover:bg-white transition-colors shadow-md"
5561
               >
5662
                 View Complete Index
components/AwardIcon.tsxadded
@@ -0,0 +1,22 @@
1
+interface AwardIconProps {
2
+  className?: string;
3
+  title?: string;
4
+}
5
+
6
+export default function AwardIcon({ className = "", title = "Award recipient" }: AwardIconProps) {
7
+  return (
8
+    <span title={title} className={`inline-block ${className}`}>
9
+      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFD619" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
10
+        {/* Ribbon top */}
11
+        <rect x="7" y="2" width="10" height="4" rx="1" />
12
+        {/* Ribbon tails */}
13
+        <path d="M7 6v4l2.5-2L7 6z" />
14
+        <path d="M17 6v4l-2.5-2L17 6z" />
15
+        {/* Medal circle */}
16
+        <circle cx="12" cy="15" r="5" />
17
+        {/* Star in medal */}
18
+        <path d="M12 12l1 2h2l-1.5 1.5.5 2.5-2-1.5-2 1.5.5-2.5L9 14h2z" />
19
+      </svg>
20
+    </span>
21
+  );
22
+}
components/Header.tsxmodified
@@ -9,9 +9,10 @@ interface HeaderProps {
99
   breadcrumbs: BreadcrumbItem[];
1010
   showSearch?: boolean;
1111
   showIndex?: boolean;
12
+  showAwards?: boolean;
1213
 }
1314
 
14
-export default function Header({ breadcrumbs, showSearch = true, showIndex = true }: HeaderProps) {
15
+export default function Header({ breadcrumbs, showSearch = true, showIndex = true, showAwards = true }: HeaderProps) {
1516
   return (
1617
     <header className="bg-[#AE122A] shadow-lg">
1718
       <div className="max-w-6xl mx-auto px-4 py-6">
@@ -38,19 +39,27 @@ export default function Header({ breadcrumbs, showSearch = true, showIndex = tru
3839
           </nav>
3940
 
4041
           {/* Right Navigation */}
41
-          {(showSearch || showIndex) && (
42
+          {(showSearch || showIndex || showAwards) && (
4243
             <div className="flex items-center space-x-4">
4344
               {showSearch && (
44
-                <Link 
45
-                  href="/memorial/search" 
45
+                <Link
46
+                  href="/memorial/search"
4647
                   className="text-[#FFD619] hover:text-white transition-colors font-semibold"
4748
                 >
4849
                   Search Memorial
4950
                 </Link>
5051
               )}
52
+              {showAwards && (
53
+                <Link
54
+                  href="/awards"
55
+                  className="bg-[#FFD619] text-[#AE122A] px-4 py-2 rounded font-bold hover:bg-white transition-colors shadow-md text-center text-sm leading-tight"
56
+                >
57
+                  Awards for<br />Heroism &amp; Gallantry
58
+                </Link>
59
+              )}
5160
               {showIndex && (
52
-                <Link 
53
-                  href="/memorial" 
61
+                <Link
62
+                  href="/memorial"
5463
                   className="bg-[#FFD619] text-[#AE122A] px-6 py-2 rounded font-bold hover:bg-white transition-colors shadow-md"
5564
                 >
5665
                   View Complete Index
lib/api.tsmodified
@@ -81,6 +81,7 @@ export interface Person {
8181
   full_display_name?: string;
8282
   death_description?: string;
8383
   pdf_key?: string;
84
+  has_awards?: boolean;
8485
 }
8586
 
8687
 export interface PersonDetail extends Person {
@@ -97,8 +98,47 @@ export interface PersonDetail extends Person {
9798
   pdf_url: string | null;
9899
 }
99100
 
101
+// Award interfaces
102
+export interface Award {
103
+  id: number;
104
+  name: string;
105
+  short_description: string;
106
+  image_filename: string;
107
+  recipient_count: number;
108
+  total_awards_given: number;
109
+  order: number;
110
+}
111
+
112
+export interface AwardRecipient {
113
+  person_id: number;
114
+  display_name: string;
115
+  full_display_name: string;
116
+  class_year: number | null;
117
+  class_letter: string;
118
+  conflict_name: string;
119
+  pdf_key: string;
120
+  count: number;
121
+  date_awarded: string | null;
122
+  citation: string;
123
+}
124
+
125
+export interface AwardDetail extends Award {
126
+  long_description: string;
127
+  recipients: AwardRecipient[];
128
+}
129
+
130
+export interface PersonAward {
131
+  award_id: number;
132
+  award_name: string;
133
+  award_image_filename: string;
134
+  count: number;
135
+  date_awarded: string | null;
136
+  citation: string;
137
+}
138
+
100139
 export interface PersonDetailWithContributions extends PersonDetail {
101140
   contributions?: Contribution[];
141
+  awards?: PersonAward[];
102142
 }
103143
 
104144
 export interface PersonSearchResult {
@@ -114,6 +154,7 @@ export interface PersonSearchResult {
114154
   conflict_name: string;
115155
   conflict_id: number;
116156
   pdf_key?: string;
157
+  has_awards?: boolean;
117158
 }
118159
 
119160
 export interface SearchFilters {
@@ -249,11 +290,30 @@ export async function getPersonContributions(personId: number): Promise<Contribu
249290
   const response = await fetch(
250291
     `${API_BASE_URL}/memorial/persons/${personId}/contributions/`
251292
   );
252
-  
293
+
253294
   if (!response.ok) {
254295
     throw new Error('Failed to fetch contributions');
255296
   }
256
-  
297
+
257298
   const data = await response.json();
258299
   return data.results || [];
300
+}
301
+
302
+// Fetch all awards
303
+export async function getAwards(): Promise<Award[]> {
304
+  const response = await fetch(`${API_BASE_URL}/memorial/awards/`);
305
+  if (!response.ok) {
306
+    throw new Error('Failed to fetch awards');
307
+  }
308
+  const data = await response.json();
309
+  return data.results || data;
310
+}
311
+
312
+// Fetch award detail with recipients
313
+export async function getAwardDetail(awardId: number): Promise<AwardDetail> {
314
+  const response = await fetch(`${API_BASE_URL}/memorial/awards/${awardId}/`);
315
+  if (!response.ok) {
316
+    throw new Error('Failed to fetch award details');
317
+  }
318
+  return response.json();
259319
 }
public/AirForceCross.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/CroixDeGuerre.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/DistinguishedServiceCross.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/DistinguishedServiceOrder.pngadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/MedailleMilitaire.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/MoHAirForce.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/MoHArmy.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/MoHNavy.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/NavyCross.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/SilverStar.pngadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).
public/VictoriaCross.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).