TypeScript · 10607 bytes Raw Blame History
1 // components/ContributionForm.tsx
2 import { useState } from 'react';
3 // alias Image to shut up the linter
4 import NextImage from 'next/image';
5 import { submitContribution, ContributionSubmit } from '@/lib/api';
6
7 interface ContributionFormProps {
8 personId: number;
9 personName: string;
10 onSuccess?: () => void;
11 }
12
13 export default function ContributionForm({ personId, personName, onSuccess }: ContributionFormProps) {
14 const [isOpen, setIsOpen] = useState(false);
15 const [formData, setFormData] = useState<ContributionSubmit>({
16 contributor_email: '',
17 content_type: 'text',
18 content_text: '',
19 });
20 const [imageFile, setImageFile] = useState<File | null>(null);
21 const [imagePreview, setImagePreview] = useState<string | null>(null);
22 const [submitting, setSubmitting] = useState(false);
23 const [error, setError] = useState<string | null>(null);
24 const [success, setSuccess] = useState(false);
25
26 const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
27 const file = e.target.files?.[0];
28 if (file) {
29 // Validate file type
30 const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
31 if (!validTypes.includes(file.type)) {
32 setError('Please select a valid image file (JPEG, PNG, GIF, or WebP)');
33 return;
34 }
35
36 // Validate file size (10MB max)
37 if (file.size > 10 * 1024 * 1024) {
38 setError('Image size must be less than 10MB');
39 return;
40 }
41
42 setImageFile(file);
43 setError(null);
44
45 // Create preview
46 const reader = new FileReader();
47 reader.onloadend = () => {
48 setImagePreview(reader.result as string);
49 };
50 reader.readAsDataURL(file);
51 }
52 };
53
54 const handleSubmit = async (e: React.FormEvent) => {
55 e.preventDefault();
56 setError(null);
57
58 // Validate required fields
59 if (!formData.contributor_email) {
60 setError('Please provide your email address');
61 return;
62 }
63
64 if (formData.content_type === 'text' && !formData.content_text) {
65 setError('Please provide text content');
66 return;
67 }
68
69 if (formData.content_type === 'image' && !imageFile) {
70 setError('Please select an image');
71 return;
72 }
73
74 if (formData.content_type === 'both' && (!formData.content_text || !imageFile)) {
75 setError('Please provide both text and image');
76 return;
77 }
78
79 setSubmitting(true);
80
81 try {
82 const submitData: ContributionSubmit = {
83 ...formData,
84 content_image: imageFile || undefined,
85 };
86
87 await submitContribution(personId, submitData);
88
89 // Success!
90 setSuccess(true);
91 setFormData({
92 contributor_email: '',
93 content_type: 'text',
94 content_text: '',
95 });
96 setImageFile(null);
97 setImagePreview(null);
98
99 // Call parent callback if provided
100 if (onSuccess) {
101 onSuccess();
102 }
103
104 // Hide success message after 5 seconds
105 setTimeout(() => {
106 setSuccess(false);
107 setIsOpen(false);
108 }, 5000);
109
110 } catch (err) {
111 setError(err instanceof Error ? err.message : 'Failed to submit contribution');
112 } finally {
113 setSubmitting(false);
114 }
115 };
116
117 return (
118 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
119 <h2 className="text-2xl font-bold mb-4 text-vmi-red">Community Contributions</h2>
120
121 {/* Toggle Button */}
122 {!isOpen && (
123 <>
124 <p className="text-gray-600 mb-4 italic">
125 No community contributions for {personName} yet.
126 </p>
127 <button
128 onClick={() => setIsOpen(true)}
129 className="bg-vmi-red text-white px-6 py-2 rounded hover:bg-vmi-dark-red transition-colors font-semibold"
130 >
131 Add Information or Image
132 </button>
133 </>
134 )}
135
136 {/* Contribution Form */}
137 {isOpen && (
138 <form onSubmit={handleSubmit} className="space-y-6 mt-6">
139 {/* Success Message */}
140 {success && (
141 <div className="bg-green-100 border-2 border-green-500 text-green-700 px-4 py-3 rounded">
142 <p className="font-semibold">Thank you for your contribution!</p>
143 <p className="text-sm">Your submission will be reviewed before being published.</p>
144 </div>
145 )}
146
147 {/* Error Message */}
148 {error && (
149 <div className="bg-red-100 border-2 border-red-500 text-red-700 px-4 py-3 rounded">
150 <p>{error}</p>
151 </div>
152 )}
153
154 {/* Email Field */}
155 <div>
156 <label htmlFor="email" className="block text-sm font-bold text-gray-700 mb-2">
157 Your Email Address *
158 </label>
159 <input
160 type="email"
161 id="email"
162 value={formData.contributor_email}
163 onChange={(e) => setFormData({ ...formData, contributor_email: e.target.value })}
164 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold"
165 placeholder="your.email@example.com"
166 required
167 />
168 <p className="text-xs text-gray-500 mt-1">
169 Your email will be partially hidden when displayed publicly
170 </p>
171 </div>
172
173 {/* Content Type Selection */}
174 <div>
175 <label className="block text-sm font-bold text-gray-700 mb-2">
176 Contribution Type *
177 </label>
178 <div className="space-y-2">
179 <label className="flex items-center cursor-pointer">
180 <input
181 type="radio"
182 value="text"
183 checked={formData.content_type === 'text'}
184 onChange={(e) => setFormData({ ...formData, content_type: e.target.value as 'text' | 'image' | 'both' })}
185 className="mr-2 text-vmi-red focus:ring-vmi-gold"
186 />
187 <span>Text Information</span>
188 </label>
189 <label className="flex items-center cursor-pointer">
190 <input
191 type="radio"
192 value="image"
193 checked={formData.content_type === 'image'}
194 onChange={(e) => setFormData({ ...formData, content_type: e.target.value as 'text' | 'image' | 'both' })}
195 className="mr-2 text-vmi-red focus:ring-vmi-gold"
196 />
197 <span>Image/Photo</span>
198 </label>
199 <label className="flex items-center cursor-pointer">
200 <input
201 type="radio"
202 value="both"
203 checked={formData.content_type === 'both'}
204 onChange={(e) => setFormData({ ...formData, content_type: e.target.value as 'text' | 'image' | 'both' })}
205 className="mr-2 text-vmi-red focus:ring-vmi-gold"
206 />
207 <span>Both Text and Image</span>
208 </label>
209 </div>
210 </div>
211
212 {/* Text Content */}
213 {(formData.content_type === 'text' || formData.content_type === 'both') && (
214 <div>
215 <label htmlFor="text" className="block text-sm font-bold text-gray-700 mb-2">
216 Additional Information *
217 </label>
218 <textarea
219 id="text"
220 value={formData.content_text}
221 onChange={(e) => setFormData({ ...formData, content_text: e.target.value })}
222 rows={6}
223 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold"
224 placeholder="Share any additional information about this person's service, life, or sacrifice..."
225 required={formData.content_type !== 'text'}
226 />
227 <p className="text-xs text-gray-500 mt-1">
228 Please be respectful and accurate. All submissions are reviewed before publication.
229 </p>
230 </div>
231 )}
232
233 {/* Image Upload */}
234 {(formData.content_type === 'image' || formData.content_type === 'both') && (
235 <div>
236 <label htmlFor="image" className="block text-sm font-bold text-gray-700 mb-2">
237 Upload Image *
238 </label>
239 <input
240 type="file"
241 id="image"
242 accept="image/jpeg,image/png,image/gif,image/webp"
243 onChange={handleImageChange}
244 className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-vmi-gold"
245 required={formData.content_type !== 'image'}
246 />
247 <p className="text-xs text-gray-500 mt-1">
248 Max 10MB. Accepted formats: JPEG, PNG, GIF, WebP
249 </p>
250
251 {/* Image Preview */}
252 {imagePreview && (
253 <div className="mt-4">
254 <p className="text-sm font-bold text-gray-700 mb-2">Preview:</p>
255 <NextImage
256 src={imagePreview}
257 alt="Preview"
258 width={400}
259 height={300}
260 className="max-w-full h-auto max-h-64 border-2 border-gray-300 rounded"
261 style={{ objectFit: 'contain' }}
262 />
263 </div>
264 )}
265 </div>
266 )}
267
268 {/* Form Actions */}
269 <div className="flex gap-4">
270 <button
271 type="submit"
272 disabled={submitting}
273 className={`px-6 py-2 rounded font-semibold transition-colors ${
274 submitting
275 ? 'bg-gray-400 text-gray-200 cursor-not-allowed'
276 : 'bg-vmi-red text-white hover:bg-vmi-dark-red'
277 }`}
278 >
279 {submitting ? 'Submitting...' : 'Submit Contribution'}
280 </button>
281 <button
282 type="button"
283 onClick={() => {
284 setIsOpen(false);
285 setError(null);
286 setSuccess(false);
287 }}
288 className="px-6 py-2 bg-gray-300 text-gray-700 rounded font-semibold hover:bg-gray-400 transition-colors"
289 >
290 Cancel
291 </button>
292 </div>
293 </form>
294 )}
295 </div>
296 );
297 }