TypeScript · 6812 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 full_display_name?: string;
81 death_description?: string;
82 }
83
84 export interface PersonDetail extends Person {
85 first_name: string;
86 middle_name: string;
87 last_name: string;
88 suffix: string;
89 date_of_death: string | null;
90 death_date_display?: string;
91 death_description: string;
92 conflict: number;
93 conflict_name: string;
94 pdf_key: string;
95 pdf_url: string | null;
96 }
97
98 export interface PersonDetailWithContributions extends PersonDetail {
99 contributions?: Contribution[];
100 }
101
102 export interface PersonSearchResult {
103 id: number;
104 display_name: string;
105 full_display_name: string;
106 class_year: number | null;
107 rank: string;
108 unit: string;
109 date_of_death: string | null;
110 death_date_display?: string;
111 conflict_name: string;
112 conflict_id: number;
113 }
114
115 export interface SearchFilters {
116 conflicts: Conflict[];
117 class_years: number[];
118 }
119
120 export interface SearchParams {
121 q?: string;
122 class_year?: string;
123 conflict?: string;
124 date_from?: string;
125 date_to?: string;
126 no_date?: boolean;
127 }
128
129 // Fetch all conflicts
130 export async function getConflicts(): Promise<Conflict[]> {
131 const response = await fetch(`${API_BASE_URL}/memorial/conflicts/`);
132 if (!response.ok) {
133 throw new Error('Failed to fetch conflicts');
134 }
135 const data = await response.json();
136 return data.results || data;
137 }
138
139 // Fetch people by conflict
140 export async function getPeopleByConflict(conflictId: number): Promise<Person[]> {
141 const response = await fetch(`${API_BASE_URL}/memorial/persons/?conflict=${conflictId}`);
142 if (!response.ok) {
143 throw new Error('Failed to fetch people');
144 }
145 const data = await response.json();
146 return data.results || data;
147 }
148
149 // Fetch person details
150 export async function getPersonDetail(personId: number): Promise<PersonDetail> {
151 const response = await fetch(`${API_BASE_URL}/memorial/persons/${personId}/`);
152 if (!response.ok) {
153 throw new Error('Failed to fetch person details');
154 }
155 return response.json();
156 }
157
158 // Fetch memorial index (all conflicts with casualties)
159 export async function getMemorialIndex(): Promise<Conflict[]> {
160 const response = await fetch(`${API_BASE_URL}/memorial/index/`);
161 if (!response.ok) {
162 throw new Error('Failed to fetch memorial index');
163 }
164 return response.json();
165 }
166
167 // Search people with filters
168 export async function searchPeople(params: SearchParams): Promise<{ count: number; results: PersonSearchResult[] }> {
169 const queryParams = new URLSearchParams();
170
171 if (params.q) queryParams.append('q', params.q);
172 if (params.class_year) queryParams.append('class_year', params.class_year);
173 if (params.conflict) queryParams.append('conflict', params.conflict);
174 if (params.date_from) queryParams.append('date_from', params.date_from);
175 if (params.date_to) queryParams.append('date_to', params.date_to);
176 if (params.no_date !== undefined) queryParams.append('no_date', params.no_date.toString());
177
178 const response = await fetch(`${API_BASE_URL}/memorial/persons/search/?${queryParams.toString()}`);
179 if (!response.ok) {
180 throw new Error('Failed to search people');
181 }
182 return response.json();
183 }
184
185 // Get available search filters
186 export async function getSearchFilters(): Promise<SearchFilters> {
187 const response = await fetch(`${API_BASE_URL}/memorial/search-filters/`);
188 if (!response.ok) {
189 throw new Error('Failed to fetch search filters');
190 }
191 return response.json();
192 }
193
194 // Submit contribution with CSRF protection
195 export async function submitContribution(
196 personId: number,
197 data: ContributionSubmit
198 ): Promise<Contribution> {
199 const formData = new FormData();
200 formData.append('contributor_email', data.contributor_email);
201 formData.append('content_type', data.content_type);
202
203 if (data.content_text) {
204 formData.append('content_text', data.content_text);
205 }
206
207 if (data.content_image) {
208 formData.append('content_image', data.content_image);
209 }
210
211 // Get CSRF token
212 const token = await getCSRFToken();
213
214 const headers: HeadersInit = {};
215 if (token) {
216 headers['X-CSRFToken'] = token;
217 }
218
219 const response = await fetch(
220 `${API_BASE_URL}/memorial/persons/${personId}/contributions/`,
221 {
222 method: 'POST',
223 body: formData,
224 headers,
225 credentials: 'include', // Important for CSRF cookies
226 }
227 );
228
229 if (!response.ok) {
230 try {
231 const error = await response.json();
232 throw new Error(error.error || error.detail || 'Failed to submit contribution');
233 } catch {
234 throw new Error(`Failed to submit contribution: ${response.statusText}`);
235 }
236 }
237
238 return response.json();
239 }
240
241 // Get approved contributions for a person
242 export async function getPersonContributions(personId: number): Promise<Contribution[]> {
243 const response = await fetch(
244 `${API_BASE_URL}/memorial/persons/${personId}/contributions/`
245 );
246
247 if (!response.ok) {
248 throw new Error('Failed to fetch contributions');
249 }
250
251 const data = await response.json();
252 return data.results || [];
253 }