vmi-virtual-memorial/vmi-wd-frontend / a130c18

Browse files

stable

Authored by espadonne
SHA
a130c1879402195371f58ac5fe2949579100125f
Parents
b5cc884
Tree
9a6a5eb

7 changed files

StatusFile+-
M app/globals.css 66 13
M app/layout.tsx 4 18
A app/memorial/conflict/[id]/page.tsx 130 0
A app/memorial/person/[id]/page.tsx 171 0
M app/page.tsx 143 92
A lib/api.ts 61 0
A public/vmi-memorial-statue.jpg bin
app/globals.cssmodified
@@ -2,26 +2,79 @@
22
 @tailwind components;
33
 @tailwind utilities;
44
 
5
+@import url('https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&family=Playfair+Display:wght@700;900&display=swap');
6
+
57
 :root {
6
-  --background: #ffffff;
7
-  --foreground: #171717;
8
+  --vmi-red: #AE122A;
9
+  --vmi-gold: #FFD619;
10
+  --vmi-dark-red: #8A0E22;
11
+  --vmi-light-gold: #FFF3B8;
12
+  --vmi-cream: #d7d4c9;
13
+  --vmi-black: #1A1A1A;
14
+  --vmi-gray: #4A4A4A;
15
+  --vmi-light-gray: #F5F5F5;
816
 }
917
 
10
-@media (prefers-color-scheme: dark) {
11
-  :root {
12
-    --background: #0a0a0a;
13
-    --foreground: #ededed;
14
-  }
18
+body {
19
+  font-family: 'Crimson Text', Georgia, serif;
20
+  color: var(--vmi-black);
21
+  background: var(--vmi-cream);
22
+  font-size: 18px;
23
+  line-height: 1.6;
1524
 }
1625
 
17
-body {
18
-  color: var(--foreground);
19
-  background: var(--background);
20
-  font-family: Arial, Helvetica, sans-serif;
26
+h1, h2, h3, h4, h5, h6 {
27
+  font-family: 'Playfair Display', Georgia, serif;
28
+  font-weight: 700;
29
+  color: var(--vmi-black);
2130
 }
2231
 
32
+/* Custom utility classes for VMI theme */
2333
 @layer utilities {
24
-  .text-balance {
25
-    text-wrap: balance;
34
+  .vmi-red {
35
+    color: var(--vmi-red);
36
+  }
37
+  
38
+  .vmi-gold {
39
+    color: var(--vmi-gold);
40
+  }
41
+  
42
+  .bg-vmi-red {
43
+    background-color: var(--vmi-red);
44
+  }
45
+  
46
+  .bg-vmi-gold {
47
+    background-color: var(--vmi-gold);
48
+  }
49
+  
50
+  .bg-vmi-dark-red {
51
+    background-color: var(--vmi-dark-red);
52
+  }
53
+  
54
+  .border-vmi-red {
55
+    border-color: var(--vmi-red);
56
+  }
57
+  
58
+  .border-vmi-gold {
59
+    border-color: var(--vmi-gold);
60
+  }
61
+  
62
+  .hover\:bg-vmi-light-gold:hover {
63
+    background-color: var(--vmi-light-gold);
64
+  }
65
+  
66
+  .bg-vmi-cream {
67
+    background-color: var(--vmi-cream);
2668
   }
2769
 }
70
+
71
+/* Override default link colors */
72
+a {
73
+  color: var(--vmi-red);
74
+  text-decoration: none;
75
+}
76
+
77
+a:hover {
78
+  color: var(--vmi-dark-red);
79
+  text-decoration: underline;
80
+}
app/layout.tsxmodified
@@ -1,21 +1,9 @@
11
 import type { Metadata } from "next";
2
-import localFont from "next/font/local";
32
 import "./globals.css";
43
 
5
-const geistSans = localFont({
6
-  src: "./fonts/GeistVF.woff",
7
-  variable: "--font-geist-sans",
8
-  weight: "100 900",
9
-});
10
-const geistMono = localFont({
11
-  src: "./fonts/GeistMonoVF.woff",
12
-  variable: "--font-geist-mono",
13
-  weight: "100 900",
14
-});
15
-
164
 export const metadata: Metadata = {
17
-  title: "Create Next App",
18
-  description: "Generated by create next app",
5
+  title: "VMI Virtual Memorial",
6
+  description: "Honoring VMI's fallen heroes who made the ultimate sacrifice in service to their country",
197
 };
208
 
219
 export default function RootLayout({
@@ -25,11 +13,9 @@ export default function RootLayout({
2513
 }>) {
2614
   return (
2715
     <html lang="en">
28
-      <body
29
-        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30
-      >
16
+      <body className="antialiased">
3117
         {children}
3218
       </body>
3319
     </html>
3420
   );
35
-}
21
+}
app/memorial/conflict/[id]/page.tsxadded
@@ -0,0 +1,130 @@
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import Link from 'next/link';
5
+import { useParams } from 'next/navigation';
6
+import { getConflicts, getPeopleByConflict, Conflict, Person } from '@/lib/api';
7
+
8
+export default function ConflictPage() {
9
+  const params = useParams();
10
+  const conflictId = parseInt(params.id as string);
11
+  
12
+  const [conflict, setConflict] = useState<Conflict | null>(null);
13
+  const [people, setPeople] = useState<Person[]>([]);
14
+  const [loading, setLoading] = useState(true);
15
+  const [error, setError] = useState<string | null>(null);
16
+
17
+  useEffect(() => {
18
+    async function fetchData() {
19
+      try {
20
+        const conflicts = await getConflicts();
21
+        const currentConflict = conflicts.find(c => c.id === conflictId);
22
+        
23
+        if (!currentConflict) {
24
+          throw new Error('Conflict not found');
25
+        }
26
+        
27
+        setConflict(currentConflict);
28
+        const peopleData = await getPeopleByConflict(conflictId);
29
+        setPeople(peopleData);
30
+      } catch (err) {
31
+        setError(err instanceof Error ? err.message : 'Failed to load data');
32
+        console.error(err);
33
+      } finally {
34
+        setLoading(false);
35
+      }
36
+    }
37
+    
38
+    fetchData();
39
+  }, [conflictId]);
40
+
41
+  if (loading) {
42
+    return (
43
+      <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
44
+        <p className="text-gray-600 text-xl">Loading...</p>
45
+      </div>
46
+    );
47
+  }
48
+
49
+  if (error || !conflict) {
50
+    return (
51
+      <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
52
+        <div className="text-center">
53
+          <p className="text-red-600 mb-4 text-xl">{error || 'Conflict not found'}</p>
54
+          <Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold">
55
+            Return to Home
56
+          </Link>
57
+        </div>
58
+      </div>
59
+    );
60
+  }
61
+
62
+  return (
63
+    <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>
76
+
77
+      {/* Main Content */}
78
+      <main className="max-w-6xl mx-auto px-4 py-12">
79
+        {/* Conflict Header */}
80
+        <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
81
+          <h1 className="text-4xl font-black text-vmi-red mb-4">
82
+            {conflict.name}
83
+          </h1>
84
+          <p className="text-xl text-gray-700 mb-4">
85
+            {conflict.start_year} – {conflict.end_year || 'Present'}
86
+          </p>
87
+          {conflict.description && (
88
+            <p className="text-gray-800 leading-relaxed mb-6">{conflict.description}</p>
89
+          )}
90
+          <div className="border-t-2 border-vmi-gold pt-6">
91
+            <p className="text-2xl font-bold text-vmi-red">
92
+              {conflict.casualty_count} VMI Alumni Gave Their Lives
93
+            </p>
94
+          </div>
95
+        </div>
96
+
97
+        {/* People List */}
98
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
99
+          <h2 className="text-3xl font-bold mb-8 text-center text-vmi-red">
100
+            Honor Roll
101
+          </h2>
102
+          
103
+          {people.length === 0 ? (
104
+            <p className="text-center text-gray-600 text-lg">No casualties recorded yet.</p>
105
+          ) : (
106
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
107
+              {people.map((person) => (
108
+                <Link
109
+                  key={person.id}
110
+                  href={`/memorial/person/${person.id}`}
111
+                  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"
112
+                >
113
+                  <h3 className="text-xl font-bold text-gray-800 group-hover:text-vmi-red transition-colors mb-2">
114
+                    {person.display_name}
115
+                  </h3>
116
+                  {person.rank && (
117
+                    <p className="text-gray-700 font-semibold">{person.rank}</p>
118
+                  )}
119
+                  {person.unit && (
120
+                    <p className="text-gray-600 text-sm italic">{person.unit}</p>
121
+                  )}
122
+                </Link>
123
+              ))}
124
+            </div>
125
+          )}
126
+        </div>
127
+      </main>
128
+    </div>
129
+  );
130
+}
app/memorial/person/[id]/page.tsxadded
@@ -0,0 +1,171 @@
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import Link from 'next/link';
5
+import { useParams } from 'next/navigation';
6
+import { getPersonDetail, PersonDetail } from '@/lib/api';
7
+
8
+export default function PersonPage() {
9
+  const params = useParams();
10
+  const personId = parseInt(params.id as string);
11
+  
12
+  const [person, setPerson] = useState<PersonDetail | null>(null);
13
+  const [loading, setLoading] = useState(true);
14
+  const [error, setError] = useState<string | null>(null);
15
+  const [pdfError, setPdfError] = useState(false);
16
+
17
+  useEffect(() => {
18
+    async function fetchData() {
19
+      try {
20
+        const personData = await getPersonDetail(personId);
21
+        setPerson(personData);
22
+      } catch (err) {
23
+        setError(err instanceof Error ? err.message : 'Failed to load person details');
24
+        console.error(err);
25
+      } finally {
26
+        setLoading(false);
27
+      }
28
+    }
29
+    
30
+    fetchData();
31
+  }, [personId]);
32
+
33
+  if (loading) {
34
+    return (
35
+      <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
36
+        <p className="text-gray-600 text-xl">Loading...</p>
37
+      </div>
38
+    );
39
+  }
40
+
41
+  if (error || !person) {
42
+    return (
43
+      <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
44
+        <div className="text-center">
45
+          <p className="text-red-600 mb-4 text-xl">{error || 'Person not found'}</p>
46
+          <Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold">
47
+            Return to Home
48
+          </Link>
49
+        </div>
50
+      </div>
51
+    );
52
+  }
53
+
54
+  const pdfUrl = person.pdf_url || '/api/memorial/persons/' + person.id + '/pdf/';
55
+
56
+  return (
57
+    <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>
77
+
78
+      {/* Main Content */}
79
+      <main className="max-w-6xl mx-auto px-4 py-12">
80
+        {/* Person Header */}
81
+        <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
82
+          <h1 className="text-4xl font-black text-vmi-red mb-6">
83
+            {person.display_name}
84
+          </h1>
85
+          
86
+          <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-gray-800">
87
+            <div className="space-y-3">
88
+              {person.rank && (
89
+                <p className="text-lg">
90
+                  <span className="font-bold text-gray-700">Rank:</span> {person.rank}
91
+                </p>
92
+              )}
93
+              {person.unit && (
94
+                <p className="text-lg">
95
+                  <span className="font-bold text-gray-700">Unit:</span> {person.unit}
96
+                </p>
97
+              )}
98
+            </div>
99
+            <div className="space-y-3">
100
+              <p className="text-lg">
101
+                <span className="font-bold text-gray-700">Conflict:</span> {person.conflict_name}
102
+              </p>
103
+              {person.date_of_death && (
104
+                <p className="text-lg">
105
+                  <span className="font-bold text-gray-700">Date of Death:</span>{' '}
106
+                  {new Date(person.date_of_death).toLocaleDateString('en-US', {
107
+                    year: 'numeric',
108
+                    month: 'long',
109
+                    day: 'numeric'
110
+                  })}
111
+                </p>
112
+              )}
113
+            </div>
114
+          </div>
115
+        </div>
116
+
117
+        {/* PDF Viewer */}
118
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
119
+          <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
120
+            Memorial Document
121
+          </h2>
122
+          
123
+          {person.pdf_key ? (
124
+            <div className="relative">
125
+              {pdfError ? (
126
+                <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
127
+                  <p className="text-gray-700 mb-4 text-lg">
128
+                    Unable to load PDF viewer. 
129
+                  </p>
130
+                  <a 
131
+                    href={pdfUrl}
132
+                    target="_blank"
133
+                    rel="noopener noreferrer"
134
+                    className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red transition-colors font-semibold"
135
+                  >
136
+                    Open PDF in New Tab
137
+                  </a>
138
+                </div>
139
+              ) : (
140
+                <div className="border-2 border-gray-400 rounded-lg overflow-hidden">
141
+                  <iframe
142
+                    src={`${pdfUrl}#toolbar=0&navpanes=0`}
143
+                    className="w-full h-[800px]"
144
+                    onError={() => setPdfError(true)}
145
+                    title={`Memorial document for ${person.display_name}`}
146
+                  />
147
+                  <div className="p-6 bg-gray-100 text-center border-t-2 border-gray-400">
148
+                    <a 
149
+                      href={pdfUrl}
150
+                      target="_blank"
151
+                      rel="noopener noreferrer"
152
+                      className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red transition-colors font-semibold"
153
+                    >
154
+                      Open PDF in Full Screen
155
+                    </a>
156
+                  </div>
157
+                </div>
158
+              )}
159
+            </div>
160
+          ) : (
161
+            <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
162
+              <p className="text-gray-700 text-lg">
163
+                No memorial document available yet.
164
+              </p>
165
+            </div>
166
+          )}
167
+        </div>
168
+      </main>
169
+    </div>
170
+  );
171
+}
app/page.tsxmodified
@@ -1,101 +1,152 @@
1
-import Image from "next/image";
1
+'use client';
2
+
3
+import { useState, useEffect } from 'react';
4
+import Link from 'next/link';
5
+import { getConflicts, Conflict } from '@/lib/api';
6
+import Image from 'next/image';
27
 
38
 export default function Home() {
9
+  const [conflicts, setConflicts] = useState<Conflict[]>([]);
10
+  const [loading, setLoading] = useState(true);
11
+  const [error, setError] = useState<string | null>(null);
12
+
13
+  useEffect(() => {
14
+    async function fetchConflicts() {
15
+      try {
16
+        const data = await getConflicts();
17
+        setConflicts(data);
18
+      } catch (err) {
19
+        setError('Failed to load conflicts');
20
+        console.error(err);
21
+      } finally {
22
+        setLoading(false);
23
+      }
24
+    }
25
+    fetchConflicts();
26
+  }, []);
27
+
428
   return (
5
-    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6
-      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
7
-        <Image
8
-          className="dark:invert"
9
-          src="https://nextjs.org/icons/next.svg"
10
-          alt="Next.js logo"
11
-          width={180}
12
-          height={38}
13
-          priority
14
-        />
15
-        <ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16
-          <li className="mb-2">
17
-            Get started by editing{" "}
18
-            <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
19
-              app/page.tsx
20
-            </code>
21
-            .
22
-          </li>
23
-          <li>Save and see your changes instantly.</li>
24
-        </ol>
29
+    <div className="min-h-screen bg-vmi-cream">
30
+      {/* Header */}
31
+      <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>
41
+            </div>
42
+          </div>
43
+          <Link href="/memorial" className="text-vmi-gold hover:text-white transition-colors font-semibold">
44
+            Memorial Index
45
+          </Link>
46
+        </div>
47
+      </header>
2548
 
26
-        <div className="flex gap-4 items-center flex-col sm:flex-row">
27
-          <a
28
-            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
29
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30
-            target="_blank"
31
-            rel="noopener noreferrer"
32
-          >
33
-            <Image
34
-              className="dark:invert"
35
-              src="https://nextjs.org/icons/vercel.svg"
36
-              alt="Vercel logomark"
37
-              width={20}
38
-              height={20}
39
-            />
40
-            Deploy now
41
-          </a>
42
-          <a
43
-            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
44
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45
-            target="_blank"
46
-            rel="noopener noreferrer"
47
-          >
48
-            Read our docs
49
-          </a>
49
+      {/* Main Content */}
50
+      <main className="max-w-5xl mx-auto px-4 py-16">
51
+        {/* Title */}
52
+        <h1 className="text-5xl font-black text-center mb-16 text-vmi-red">
53
+          VMI Virtual Memorial
54
+        </h1>
55
+
56
+        {/* Welcome Card */}
57
+        <div className="bg-white border-2 border-vmi-gold rounded-lg p-10 mb-16 shadow-xl">
58
+          <div className="max-w-3xl mx-auto text-center">
59
+            <p className="text-xl text-gray-800 mb-8 leading-relaxed">
60
+              Since 1839, the Virginia Military Institute has produced leaders of character who have served their country in peace and at war.
61
+              From the Mexican War to the War on Terrorism, VMI cadets and alumni have answered the call to service.
62
+            </p>
63
+
64
+            <p className="text-xl text-gray-800 mb-8 leading-relaxed">
65
+              This virtual memorial lists the names of VMI Alumni who died on the Field of Honor.
66
+              Their class year is shown with their names.
67
+              Links for those highlighted provide more information on their story and how they “Gave All.”
68
+            </p>
69
+
70
+            {/* Placeholder for statue image */}
71
+            <div className="relative w-full h-[600px] mb-6 rounded-lg overflow-hidden border-4 border-white shadow-inner">
72
+              <Image
73
+                src="/vmi-memorial-statue.jpg"
74
+                alt="VMI Memorial Statue - Virginia Mourning Her Dead"
75
+                fill
76
+                className="object-cover object-top"
77
+                priority
78
+              />
79
+            </div>
80
+            <p className="text-2xl text-vmi-red font-bold italic">
81
+              "In Pace Paratus"
82
+            </p>
83
+            <p className="text-lg text-gray-700">
84
+              Prepared in Peace
85
+            </p>
86
+          </div>
87
+        </div>
88
+
89
+        {/* Conflicts List */}
90
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-10 shadow-xl">
91
+          <h2 className="text-3xl font-bold mb-8 text-center text-vmi-red">
92
+            Browse by Conflict
93
+          </h2>
94
+
95
+          {loading && (
96
+            <p className="text-center text-gray-600">Loading conflicts...</p>
97
+          )}
98
+
99
+          {error && (
100
+            <p className="text-center text-red-600">{error}</p>
101
+          )}
102
+
103
+          {!loading && !error && conflicts.length === 0 && (
104
+            <p className="text-center text-gray-600">No conflicts found. Please add some through the admin panel.</p>
105
+          )}
106
+
107
+          {!loading && !error && conflicts.length > 0 && (
108
+            <ul className="space-y-4">
109
+              {conflicts.map((conflict) => (
110
+                <li key={conflict.id}>
111
+                  <Link
112
+                    href={`/memorial/conflict/${conflict.id}`}
113
+                    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"
114
+                  >
115
+                    <div className="flex justify-between items-center">
116
+                      <div>
117
+                        <h3 className="text-2xl font-bold text-gray-800 group-hover:text-vmi-red transition-colors">
118
+                          {conflict.name}
119
+                        </h3>
120
+                        <p className="text-gray-600">
121
+                          {conflict.start_year} – {conflict.end_year || 'Present'}
122
+                        </p>
123
+                      </div>
124
+                      <div className="text-right">
125
+                        <p className="text-4xl font-black text-vmi-red">
126
+                          {conflict.casualty_count}
127
+                        </p>
128
+                        <p className="text-sm text-gray-600 uppercase tracking-wide">Casualties</p>
129
+                      </div>
130
+                    </div>
131
+                  </Link>
132
+                </li>
133
+              ))}
134
+            </ul>
135
+          )}
50136
         </div>
51137
       </main>
52
-      <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
53
-        <a
54
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
55
-          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56
-          target="_blank"
57
-          rel="noopener noreferrer"
58
-        >
59
-          <Image
60
-            aria-hidden
61
-            src="https://nextjs.org/icons/file.svg"
62
-            alt="File icon"
63
-            width={16}
64
-            height={16}
65
-          />
66
-          Learn
67
-        </a>
68
-        <a
69
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
70
-          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
71
-          target="_blank"
72
-          rel="noopener noreferrer"
73
-        >
74
-          <Image
75
-            aria-hidden
76
-            src="https://nextjs.org/icons/window.svg"
77
-            alt="Window icon"
78
-            width={16}
79
-            height={16}
80
-          />
81
-          Examples
82
-        </a>
83
-        <a
84
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
85
-          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
86
-          target="_blank"
87
-          rel="noopener noreferrer"
88
-        >
89
-          <Image
90
-            aria-hidden
91
-            src="https://nextjs.org/icons/globe.svg"
92
-            alt="Globe icon"
93
-            width={16}
94
-            height={16}
95
-          />
96
-          Go to nextjs.org →
97
-        </a>
138
+
139
+      {/* Footer */}
140
+      <footer className="bg-gray-900 text-white py-8 mt-16">
141
+        <div className="max-w-5xl mx-auto px-4 text-center">
142
+          <p className="text-sm">
143
+            © {new Date().getFullYear()} Virginia Military Institute. All rights reserved.
144
+          </p>
145
+          <p className="text-xs mt-2 text-gray-400">
146
+            "That I May Give Evidence of My Principles Through My Conduct"
147
+          </p>
148
+        </div>
98149
       </footer>
99150
     </div>
100151
   );
101
-}
152
+}
lib/api.tsadded
@@ -0,0 +1,61 @@
1
+// API configuration for the VMI Memorial frontend
2
+
3
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
4
+
5
+export interface Conflict {
6
+  id: number;
7
+  name: string;
8
+  start_year: number;
9
+  end_year: number | null;
10
+  description: string;
11
+  casualty_count: number;
12
+  order: number;
13
+}
14
+
15
+export interface Person {
16
+  id: number;
17
+  display_name: string;
18
+  rank: string;
19
+  unit: string;
20
+}
21
+
22
+export interface PersonDetail extends Person {
23
+  first_name: string;
24
+  middle_name: string;
25
+  last_name: string;
26
+  suffix: string;
27
+  date_of_death: string | null;
28
+  conflict: number;
29
+  conflict_name: string;
30
+  pdf_key: string;
31
+  pdf_url: string | null;
32
+}
33
+
34
+// Fetch all conflicts
35
+export async function getConflicts(): Promise<Conflict[]> {
36
+  const response = await fetch(`${API_BASE_URL}/memorial/conflicts/`);
37
+  if (!response.ok) {
38
+    throw new Error('Failed to fetch conflicts');
39
+  }
40
+  const data = await response.json();
41
+  return data.results || data;
42
+}
43
+
44
+// Fetch people by conflict
45
+export async function getPeopleByConflict(conflictId: number): Promise<Person[]> {
46
+  const response = await fetch(`${API_BASE_URL}/memorial/persons/?conflict=${conflictId}`);
47
+  if (!response.ok) {
48
+    throw new Error('Failed to fetch people');
49
+  }
50
+  const data = await response.json();
51
+  return data.results || data;
52
+}
53
+
54
+// Fetch person details
55
+export async function getPersonDetail(personId: number): Promise<PersonDetail> {
56
+  const response = await fetch(`${API_BASE_URL}/memorial/persons/${personId}/`);
57
+  if (!response.ok) {
58
+    throw new Error('Failed to fetch person details');
59
+  }
60
+  return response.json();
61
+}
public/vmi-memorial-statue.jpgadded
Image file changed (preview rendering wires once /raw URLs are threaded into the diff renderer).