TypeScript · 8476 bytes Raw Blame History
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 // CSRF token management
6 let csrfToken: string | null = null;
7
8 // Helper function to get CSRF token from cookies
9 function getCookie(name: string): string | null {
10 if (typeof document === 'undefined') return null;
11
12 const value = `; ${document.cookie}`;
13 const parts = value.split(`; ${name}=`);
14 if (parts.length === 2) {
15 return parts.pop()?.split(';').shift() || null;
16 }
17 return null;
18 }
19
20 // Get CSRF token from API or cookie
21 async function getCSRFToken(): Promise<string | null> {
22 if (csrfToken) return csrfToken;
23
24 // First try to get from cookie
25 const cookieToken = getCookie('csrftoken');
26 if (cookieToken) {
27 csrfToken = cookieToken;
28 return csrfToken;
29 }
30
31 // If no cookie, get from API
32 try {
33 const response = await fetch(`${API_BASE_URL}/csrf/`, {
34 credentials: 'include',
35 });
36 if (response.ok) {
37 const data = await response.json();
38 csrfToken = data.csrfToken;
39 return csrfToken;
40 }
41 } catch (error) {
42 console.error('Failed to get CSRF token:', error);
43 }
44
45 return null;
46 }
47
48 export interface Contribution {
49 id: number;
50 content_type: 'text' | 'image' | 'both';
51 content_text?: string;
52 content_image?: string;
53 submitted_at: string;
54 contributor_display: string;
55 }
56
57 export interface ContributionSubmit {
58 contributor_email: string;
59 content_type: 'text' | 'image' | 'both';
60 content_text?: string;
61 content_image?: File;
62 }
63
64 export interface Conflict {
65 id: number;
66 name: string;
67 start_year: number;
68 end_year: number | null;
69 description: string;
70 casualty_count: number;
71 order: number;
72 }
73
74 export interface Person {
75 id: number;
76 display_name: string;
77 rank: string;
78 unit: string;
79 class_year?: number;
80 class_letter?: string;
81 full_display_name?: string;
82 death_description?: string;
83 pdf_key?: string;
84 has_awards?: boolean;
85 }
86
87 export interface PersonDetail extends Person {
88 first_name: string;
89 middle_name: string;
90 last_name: string;
91 suffix: string;
92 date_of_death: string | null;
93 death_date_display?: string;
94 death_description: string;
95 conflict: number;
96 conflict_name: string;
97 pdf_key: string;
98 pdf_url: string | null;
99 }
100
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
139 export interface PersonDetailWithContributions extends PersonDetail {
140 contributions?: Contribution[];
141 awards?: PersonAward[];
142 }
143
144 export interface PersonSearchResult {
145 id: number;
146 display_name: string;
147 full_display_name: string;
148 class_year: number | null;
149 class_letter?: string;
150 rank: string;
151 unit: string;
152 date_of_death: string | null;
153 death_date_display?: string;
154 conflict_name: string;
155 conflict_id: number;
156 pdf_key?: string;
157 has_awards?: boolean;
158 }
159
160 export interface SearchFilters {
161 conflicts: Conflict[];
162 class_years: number[];
163 }
164
165 export interface SearchParams {
166 q?: string;
167 class_year?: string;
168 conflict?: string;
169 date_from?: string;
170 date_to?: string;
171 no_date?: boolean;
172 has_document?: boolean;
173 }
174
175 // Fetch all conflicts
176 export async function getConflicts(): Promise<Conflict[]> {
177 const response = await fetch(`${API_BASE_URL}/memorial/conflicts/`);
178 if (!response.ok) {
179 throw new Error('Failed to fetch conflicts');
180 }
181 const data = await response.json();
182 return data.results || data;
183 }
184
185 // Fetch people by conflict
186 export async function getPeopleByConflict(conflictId: number): Promise<PersonDetail[]> {
187 const response = await fetch(`${API_BASE_URL}/memorial/persons/?conflict=${conflictId}&paginate=false`);
188 if (!response.ok) {
189 throw new Error('Failed to fetch people');
190 }
191 const data = await response.json();
192 return data.results || data;
193 }
194
195 // Fetch person details
196 export async function getPersonDetail(personId: number): Promise<PersonDetail> {
197 const response = await fetch(`${API_BASE_URL}/memorial/persons/${personId}/`);
198 if (!response.ok) {
199 throw new Error('Failed to fetch person details');
200 }
201 return response.json();
202 }
203
204 // Fetch memorial index (all conflicts with casualties)
205 export async function getMemorialIndex(): Promise<Conflict[]> {
206 const response = await fetch(`${API_BASE_URL}/memorial/index/`);
207 if (!response.ok) {
208 throw new Error('Failed to fetch memorial index');
209 }
210 return response.json();
211 }
212
213 // Search people with filters
214 export async function searchPeople(params: SearchParams): Promise<{ count: number; results: PersonDetail[] }> {
215 const queryParams = new URLSearchParams();
216
217 if (params.q) queryParams.append('q', params.q);
218 if (params.class_year) queryParams.append('class_year', params.class_year);
219 if (params.conflict) queryParams.append('conflict', params.conflict);
220 if (params.date_from) queryParams.append('date_from', params.date_from);
221 if (params.date_to) queryParams.append('date_to', params.date_to);
222 if (params.no_date !== undefined) queryParams.append('no_date', params.no_date.toString());
223 if (params.has_document !== undefined) queryParams.append('has_document', params.has_document.toString());
224
225 const response = await fetch(`${API_BASE_URL}/memorial/persons/search/?${queryParams.toString()}`);
226 if (!response.ok) {
227 throw new Error('Failed to search people');
228 }
229 return response.json();
230 }
231
232 // Get available search filters
233 export async function getSearchFilters(): Promise<SearchFilters> {
234 const response = await fetch(`${API_BASE_URL}/memorial/search-filters/`);
235 if (!response.ok) {
236 throw new Error('Failed to fetch search filters');
237 }
238 return response.json();
239 }
240
241 // Submit contribution with CSRF protection
242 export async function submitContribution(
243 personId: number,
244 data: ContributionSubmit
245 ): Promise<Contribution> {
246 const formData = new FormData();
247 formData.append('contributor_email', data.contributor_email);
248 formData.append('content_type', data.content_type);
249
250 if (data.content_text) {
251 formData.append('content_text', data.content_text);
252 }
253
254 if (data.content_image) {
255 formData.append('content_image', data.content_image);
256 }
257
258 // Get CSRF token
259 const token = await getCSRFToken();
260
261 const headers: HeadersInit = {};
262 if (token) {
263 headers['X-CSRFToken'] = token;
264 }
265
266 const response = await fetch(
267 `${API_BASE_URL}/memorial/persons/${personId}/contributions/`,
268 {
269 method: 'POST',
270 body: formData,
271 headers,
272 credentials: 'include', // Important for CSRF cookies
273 }
274 );
275
276 if (!response.ok) {
277 try {
278 const error = await response.json();
279 throw new Error(error.error || error.detail || 'Failed to submit contribution');
280 } catch {
281 throw new Error(`Failed to submit contribution: ${response.statusText}`);
282 }
283 }
284
285 return response.json();
286 }
287
288 // Get approved contributions for a person
289 export async function getPersonContributions(personId: number): Promise<Contribution[]> {
290 const response = await fetch(
291 `${API_BASE_URL}/memorial/persons/${personId}/contributions/`
292 );
293
294 if (!response.ok) {
295 throw new Error('Failed to fetch contributions');
296 }
297
298 const data = await response.json();
299 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();
319 }