vmi-virtual-memorial/vmi-wd-frontend / 06ebec9

Browse files

submission feature stable

Authored by espadonne
SHA
06ebec98a254b25478fe0c7122cd527ca52db1e7
Parents
21f6f88
Tree
7101f16

4 changed files

StatusFile+-
M app/memorial/person/[id]/page.tsx 34 7
A components/ContributionDisplay.tsx 97 0
A components/ContributionForm.tsx 297 0
M lib/api.ts 125 5
app/memorial/person/[id]/page.tsxmodified
@@ -3,14 +3,16 @@
33
 import { useState, useEffect } from 'react';
44
 import Link from 'next/link';
55
 import { useParams } from 'next/navigation';
6
-import { getPersonDetail, PersonDetail } from '@/lib/api';
6
+import { getPersonDetail, PersonDetailWithContributions } from '@/lib/api';
77
 import Header from '@/components/Header';
8
+import ContributionForm from '@/components/ContributionForm';
9
+import ContributionDisplay from '@/components/ContributionDisplay';
810
 
911
 export default function PersonPage() {
1012
   const params = useParams();
1113
   const personId = parseInt(params.id as string);
1214
   
13
-  const [person, setPerson] = useState<PersonDetail | null>(null);
15
+  const [person, setPerson] = useState<PersonDetailWithContributions | null>(null);
1416
   const [loading, setLoading] = useState(true);
1517
   const [error, setError] = useState<string | null>(null);
1618
   const [pdfError, setPdfError] = useState(false);
@@ -39,7 +41,6 @@ export default function PersonPage() {
3941
         setLoadingPdf(true);
4042
         try {
4143
           const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
42
-          // FIXED: Just set the URL directly - don't try to fetch and parse as JSON
4344
           setPdfUrl(`${apiUrl}/memorial/persons/${person.id}/pdf/`);
4445
         } catch (err) {
4546
           console.error('Failed to set PDF URL:', err);
@@ -53,6 +54,16 @@ export default function PersonPage() {
5354
     fetchPdfUrl();
5455
   }, [person, pdfUrl]);
5556
 
57
+  const handleContributionSuccess = async () => {
58
+    // Reload person data to get updated contributions
59
+    try {
60
+      const personData = await getPersonDetail(personId);
61
+      setPerson(personData);
62
+    } catch (err) {
63
+      console.error('Failed to reload person data:', err);
64
+    }
65
+  };
66
+
5667
   if (loading) {
5768
     return (
5869
       <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
@@ -74,6 +85,10 @@ export default function PersonPage() {
7485
     );
7586
   }
7687
 
88
+  const displayName = person.full_display_name ? 
89
+    person.full_display_name.replace(person.rank + ' ', '').replace(person.rank + ', ', '') 
90
+    : person.display_name;
91
+
7792
   return (
7893
     <div className="min-h-screen bg-vmi-cream">
7994
       <Header 
@@ -89,9 +104,7 @@ export default function PersonPage() {
89104
         {/* Person Header */}
90105
         <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
91106
           <h1 className="text-4xl font-black text-vmi-red mb-2">
92
-            {person.full_display_name ? 
93
-              person.full_display_name.replace(person.rank + ' ', '').replace(person.rank + ', ', '') 
94
-              : person.display_name}
107
+            {displayName}
95108
           </h1>
96109
           
97110
           {/* Rank and Unit subtitle */}
@@ -141,7 +154,7 @@ export default function PersonPage() {
141154
         )}
142155
 
143156
         {/* PDF Viewer */}
144
-        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl">
157
+        <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mb-12">
145158
           <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
146159
             Memorial Document
147160
           </h2>
@@ -197,6 +210,20 @@ export default function PersonPage() {
197210
             </div>
198211
           )}
199212
         </div>
213
+
214
+        {/* Community Contributions Section */}
215
+        <ContributionForm 
216
+          personId={person.id} 
217
+          personName={displayName}
218
+          onSuccess={handleContributionSuccess}
219
+        />
220
+        
221
+        {/* Display Approved Contributions */}
222
+        {person.contributions && person.contributions.length > 0 && (
223
+          <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mt-8">
224
+            <ContributionDisplay contributions={person.contributions} />
225
+          </div>
226
+        )}
200227
       </main>
201228
     </div>
202229
   );
components/ContributionDisplay.tsxadded
@@ -0,0 +1,97 @@
1
+// components/ContributionDisplay.tsx
2
+
3
+// alias Image to shut the linter up
4
+import NextImage from 'next/image';
5
+import { Contribution } from '@/lib/api';
6
+
7
+
8
+interface ContributionDisplayProps {
9
+  contributions: Contribution[];
10
+}
11
+
12
+export default function ContributionDisplay({ contributions }: ContributionDisplayProps) {
13
+  if (contributions.length === 0) {
14
+    return null;
15
+  }
16
+
17
+  const formatDate = (dateString: string) => {
18
+    const date = new Date(dateString);
19
+    return date.toLocaleDateString('en-US', {
20
+      year: 'numeric',
21
+      month: 'long',
22
+      day: 'numeric'
23
+    });
24
+  };
25
+
26
+  return (
27
+    <div className="space-y-6">
28
+      <h3 className="text-xl font-bold text-vmi-red mb-4">
29
+        Community Contributions ({contributions.length})
30
+      </h3>
31
+      
32
+      <div className="space-y-4">
33
+        {contributions.map((contribution) => (
34
+          <div 
35
+            key={contribution.id} 
36
+            className="bg-gray-50 border border-gray-200 rounded-lg p-6"
37
+          >
38
+            {/* Header */}
39
+            <div className="flex justify-between items-start mb-4">
40
+              <div>
41
+                <p className="text-sm text-gray-600">
42
+                  Contributed by <span className="font-semibold">{contribution.contributor_display}</span>
43
+                </p>
44
+                <p className="text-xs text-gray-500">
45
+                  {formatDate(contribution.submitted_at)}
46
+                </p>
47
+              </div>
48
+              <span className={`
49
+                px-3 py-1 rounded-full text-xs font-semibold
50
+                ${contribution.content_type === 'text' ? 'bg-blue-100 text-blue-700' : ''}
51
+                ${contribution.content_type === 'image' ? 'bg-green-100 text-green-700' : ''}
52
+                ${contribution.content_type === 'both' ? 'bg-purple-100 text-purple-700' : ''}
53
+              `}>
54
+                {contribution.content_type === 'text' && 'Text'}
55
+                {contribution.content_type === 'image' && 'Image'}
56
+                {contribution.content_type === 'both' && 'Text & Image'}
57
+              </span>
58
+            </div>
59
+            
60
+            {/* Content */}
61
+            <div className="space-y-4">
62
+              {/* Text Content */}
63
+              {contribution.content_text && (
64
+                <div className="prose prose-gray max-w-none">
65
+                  <p className="text-gray-800 whitespace-pre-wrap">
66
+                    {contribution.content_text}
67
+                  </p>
68
+                </div>
69
+              )}
70
+              {/* Image Content */}
71
+              {contribution.content_image && (
72
+                <div className="mt-4">
73
+                  <div 
74
+                    className="cursor-pointer hover:opacity-95 transition-opacity"
75
+                    onClick={() => window.open(contribution.content_image, '_blank')}
76
+                  >
77
+                    <NextImage 
78
+                      src={contribution.content_image} 
79
+                      alt="Community contribution"
80
+                      width={800}
81
+                      height={600}
82
+                      className="max-w-full h-auto rounded-lg border-2 border-gray-300"
83
+                      style={{ objectFit: 'contain' }}
84
+                    />
85
+                  </div>
86
+                  <p className="text-xs text-gray-500 mt-2">
87
+                    Click image to view full size
88
+                  </p>
89
+                </div>
90
+              )}
91
+            </div>
92
+          </div>
93
+        ))}
94
+      </div>
95
+    </div>
96
+  );
97
+}
components/ContributionForm.tsxadded
@@ -0,0 +1,297 @@
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 !== 'image'}
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 !== 'text'}
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
+}
lib/api.tsmodified
@@ -1,8 +1,65 @@
11
 // API configuration for the VMI Memorial frontend
22
 
33
 const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
4
-// const API_BASE_URL = 'https://web-production-6002.up.railway.app/api';
54
 
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
+}
663
 
764
 export interface Conflict {
865
   id: number;
@@ -24,14 +81,13 @@ export interface Person {
2481
   death_description?: string;
2582
 }
2683
 
27
-// CHANGED: Added death_date_display?: string
2884
 export interface PersonDetail extends Person {
2985
   first_name: string;
3086
   middle_name: string;
3187
   last_name: string;
3288
   suffix: string;
3389
   date_of_death: string | null;
34
-  death_date_display?: string;  // <-- ADDED THIS LINE
90
+  death_date_display?: string;
3591
   death_description: string;
3692
   conflict: number;
3793
   conflict_name: string;
@@ -39,7 +95,10 @@ export interface PersonDetail extends Person {
3995
   pdf_url: string | null;
4096
 }
4197
 
42
-// CHANGED: Added death_date_display?: string
98
+export interface PersonDetailWithContributions extends PersonDetail {
99
+  contributions?: Contribution[];
100
+}
101
+
43102
 export interface PersonSearchResult {
44103
   id: number;
45104
   display_name: string;
@@ -48,7 +107,7 @@ export interface PersonSearchResult {
48107
   rank: string;
49108
   unit: string;
50109
   date_of_death: string | null;
51
-  death_date_display?: string;  // <-- ADDED THIS LINE
110
+  death_date_display?: string;
52111
   conflict_name: string;
53112
   conflict_id: number;
54113
 }
@@ -130,4 +189,65 @@ export async function getSearchFilters(): Promise<SearchFilters> {
130189
     throw new Error('Failed to fetch search filters');
131190
   }
132191
   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 || [];
133253
 }