1+ 'use client'
2+
3+ import { useState , useEffect } from 'react'
4+ import { supabase } from '@/lib/supabaseClient'
5+
6+ interface CampApplication {
7+ id : string
8+ first_name : string
9+ last_name : string
10+ nickname : string
11+ gender : string
12+ birth_date : string
13+ question1 : string
14+ question2 : string
15+ question3 : string
16+ status : 'pending' | 'approve' | 'decline'
17+ comment : string
18+ certificate : boolean
19+ certificate_url : string
20+ }
21+
22+ export default function CampSelection ( ) {
23+ const [ applications , setApplications ] = useState < CampApplication [ ] > ( [ ] )
24+ const [ loading , setLoading ] = useState ( true )
25+ const [ editingId , setEditingId ] = useState < string | null > ( null )
26+ const [ editComment , setEditComment ] = useState ( '' )
27+ const [ bulkCertificate , setBulkCertificate ] = useState ( false )
28+ const [ uploadingTemplate , setUploadingTemplate ] = useState ( false )
29+ const [ templateFile , setTemplateFile ] = useState < File | null > ( null )
30+ const [ previewUrl , setPreviewUrl ] = useState < string > ( '' )
31+
32+ useEffect ( ( ) => {
33+ fetchApplications ( )
34+ } , [ ] )
35+
36+ const fetchApplications = async ( ) => {
37+ try {
38+ const { data, error } = await supabase
39+ . from ( 'camp1_applications' )
40+ . select ( '*' )
41+ . order ( 'submitted_at' , { ascending : false } )
42+
43+ if ( error ) throw error
44+ setApplications ( data || [ ] )
45+ } catch ( error ) {
46+ console . error ( 'Error fetching applications:' , error )
47+ alert ( 'Error fetching applications' )
48+ } finally {
49+ setLoading ( false )
50+ }
51+ }
52+
53+ const updateStatus = async ( id : string , status : 'pending' | 'approve' | 'decline' ) => {
54+ try {
55+ const { error } = await supabase
56+ . from ( 'camp1_applications' )
57+ . update ( { status } )
58+ . eq ( 'id' , id )
59+
60+ if ( error ) throw error
61+ fetchApplications ( )
62+ } catch ( error ) {
63+ console . error ( 'Error updating status:' , error )
64+ alert ( 'Error updating status' )
65+ }
66+ }
67+
68+ const updateComment = async ( id : string ) => {
69+ try {
70+ const { error } = await supabase
71+ . from ( 'camp1_applications' )
72+ . update ( { comment : editComment } )
73+ . eq ( 'id' , id )
74+
75+ if ( error ) throw error
76+ setEditingId ( null )
77+ setEditComment ( '' )
78+ fetchApplications ( )
79+ } catch ( error ) {
80+ console . error ( 'Error updating comment:' , error )
81+ alert ( 'Error updating comment' )
82+ }
83+ }
84+
85+ const updateCertificateUrl = async ( id : string , url : string ) => {
86+ try {
87+ const { error } = await supabase
88+ . from ( 'camp1_applications' )
89+ . update ( { certificate_url : url } )
90+ . eq ( 'id' , id )
91+
92+ if ( error ) throw error
93+ fetchApplications ( )
94+ } catch ( error ) {
95+ console . error ( 'Error updating certificate URL:' , error )
96+ alert ( 'Error updating certificate URL' )
97+ }
98+ }
99+
100+ const toggleBulkCertificate = async ( ) => {
101+ try {
102+ const { error } = await supabase
103+ . from ( 'camp1_applications' )
104+ . update ( {
105+ certificate : ! bulkCertificate ,
106+ submitted_at : new Date ( ) . toISOString ( )
107+ } )
108+ . not ( 'id' , 'is' , null )
109+
110+ if ( error ) throw error
111+ setBulkCertificate ( ! bulkCertificate )
112+ fetchApplications ( )
113+ } catch ( error ) {
114+ console . error ( 'Error updating bulk certificate:' , error )
115+ alert ( 'Error updating bulk certificate' )
116+ }
117+ }
118+
119+ const handleTemplateFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
120+ if ( e . target . files && e . target . files [ 0 ] ) {
121+ const file = e . target . files [ 0 ]
122+ if ( ! file . type . startsWith ( 'image/' ) ) {
123+ alert ( 'Please upload an image file (JPG, PNG, etc)' )
124+ return
125+ }
126+ setTemplateFile ( file )
127+ setPreviewUrl ( URL . createObjectURL ( file ) )
128+ }
129+ }
130+
131+ const uploadTemplateAndUpdateUrls = async ( ) => {
132+ if ( ! templateFile ) return
133+
134+ setUploadingTemplate ( true )
135+ try {
136+ // Upload template to Supabase Storage
137+ const fileExt = templateFile . name . split ( '.' ) . pop ( )
138+ const fileName = `certificate-template-${ Date . now ( ) } .${ fileExt } `
139+
140+ const { data : uploadData , error : uploadError } = await supabase . storage
141+ . from ( 'certificates' )
142+ . upload ( `templates/${ fileName } ` , templateFile )
143+
144+ if ( uploadError ) throw uploadError
145+
146+ // Get the public URL
147+ const { data : { publicUrl } } = supabase . storage
148+ . from ( 'certificates' )
149+ . getPublicUrl ( `templates/${ fileName } ` )
150+
151+ // Update all applications with the new template URL
152+ const { error : updateError } = await supabase
153+ . from ( 'camp1_applications' )
154+ . update ( {
155+ certificate_url : publicUrl ,
156+ submitted_at : new Date ( ) . toISOString ( )
157+ } )
158+ . not ( 'id' , 'is' , null ) // Better way to select all rows
159+
160+ if ( updateError ) throw updateError
161+
162+ // Refresh the applications list
163+ fetchApplications ( )
164+
165+ // Clear the file input
166+ setTemplateFile ( null )
167+ setPreviewUrl ( '' )
168+
169+ alert ( 'Certificate template updated successfully!' )
170+ } catch ( error ) {
171+ console . error ( 'Error uploading template:' , error )
172+ alert ( 'Error uploading certificate template' )
173+ } finally {
174+ setUploadingTemplate ( false )
175+ }
176+ }
177+
178+ const getStatusColor = ( status : string ) => {
179+ switch ( status ) {
180+ case 'approve' :
181+ return 'bg-green-100 text-green-800'
182+ case 'decline' :
183+ return 'bg-red-100 text-red-800'
184+ default :
185+ return 'bg-yellow-100 text-yellow-800'
186+ }
187+ }
188+
189+ if ( loading ) {
190+ return (
191+ < div className = "min-h-screen flex items-center justify-center" >
192+ < div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900" > </ div >
193+ </ div >
194+ )
195+ }
196+
197+ return (
198+ < div className = "min-h-screen bg-gray-100 py-8 px-4 sm:px-6 lg:px-8" >
199+ < div className = "max-w-7xl mx-auto" >
200+ < div className = "mb-6" >
201+ < h1 className = "text-3xl font-bold text-gray-900 mb-4" > Camp Applications</ h1 >
202+ < div className = "flex flex-wrap gap-4 items-start bg-white p-4 rounded-lg shadow" >
203+ < div className = "flex-1 min-w-[300px]" >
204+ < label className = "block text-sm font-medium text-gray-700 mb-1" > Certificate Template (Image)</ label >
205+ < div className = "flex flex-col gap-2" >
206+ < div className = "flex gap-2" >
207+ < input
208+ type = "file"
209+ accept = "image/*"
210+ onChange = { handleTemplateFileChange }
211+ className = "text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
212+ />
213+ < button
214+ onClick = { uploadTemplateAndUpdateUrls }
215+ disabled = { ! templateFile || uploadingTemplate }
216+ className = "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
217+ >
218+ { uploadingTemplate ? 'Uploading...' : 'Upload & Apply' }
219+ </ button >
220+ </ div >
221+ { previewUrl && (
222+ < div className = "mt-2" >
223+ < p className = "text-sm text-gray-500 mb-1" > Selected template:</ p >
224+ < div className = "flex items-center gap-2" >
225+ < svg className = "w-6 h-6 text-red-500" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
226+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
227+ </ svg >
228+ < span className = "text-sm text-gray-700" > { templateFile ?. name } </ span >
229+ < button
230+ onClick = { ( ) => {
231+ setTemplateFile ( null )
232+ setPreviewUrl ( '' )
233+ } }
234+ className = "text-xs text-red-600 hover:text-red-800"
235+ >
236+ Remove
237+ </ button >
238+ </ div >
239+ </ div >
240+ ) }
241+ </ div >
242+ </ div >
243+ < div className = "flex items-center" >
244+ < button
245+ onClick = { toggleBulkCertificate }
246+ className = "px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors whitespace-nowrap"
247+ >
248+ { bulkCertificate ? 'Disable All Certificates' : 'Enable All Certificates' }
249+ </ button >
250+ </ div >
251+ </ div >
252+ </ div >
253+
254+ < div className = "bg-white shadow-xl rounded-lg overflow-hidden" >
255+ < div className = "overflow-x-auto" >
256+ < table className = "min-w-full divide-y divide-gray-200" >
257+ < thead className = "bg-gray-50" >
258+ < tr >
259+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Name</ th >
260+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Details</ th >
261+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Status</ th >
262+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Comment</ th >
263+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Certificate</ th >
264+ < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" > Actions</ th >
265+ </ tr >
266+ </ thead >
267+ < tbody className = "bg-white divide-y divide-gray-200" >
268+ { applications . map ( ( app ) => (
269+ < tr key = { app . id } >
270+ < td className = "px-6 py-4 whitespace-nowrap" >
271+ < div className = "text-sm font-medium text-gray-900" > { app . first_name } { app . last_name } </ div >
272+ < div className = "text-sm text-gray-500" > { app . nickname } </ div >
273+ </ td >
274+ < td className = "px-6 py-4" >
275+ < div className = "text-sm text-gray-900" > { app . gender } </ div >
276+ < div className = "text-sm text-gray-500" > { new Date ( app . birth_date ) . toLocaleDateString ( ) } </ div >
277+ </ td >
278+ < td className = "px-6 py-4 whitespace-nowrap" >
279+ < span className = { `px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${ getStatusColor ( app . status ) } ` } >
280+ { app . status }
281+ </ span >
282+ < div className = "mt-2 space-x-2" >
283+ < button
284+ onClick = { ( ) => updateStatus ( app . id , 'approve' ) }
285+ className = "text-xs bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600"
286+ >
287+ Approve
288+ </ button >
289+ < button
290+ onClick = { ( ) => updateStatus ( app . id , 'decline' ) }
291+ className = "text-xs bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
292+ >
293+ Decline
294+ </ button >
295+ < button
296+ onClick = { ( ) => updateStatus ( app . id , 'pending' ) }
297+ className = "text-xs bg-yellow-500 text-white px-2 py-1 rounded hover:bg-yellow-600"
298+ >
299+ Pending
300+ </ button >
301+ </ div >
302+ </ td >
303+ < td className = "px-6 py-4" >
304+ { editingId === app . id ? (
305+ < div className = "flex items-center space-x-2" >
306+ < input
307+ type = "text"
308+ value = { editComment }
309+ onChange = { ( e ) => setEditComment ( e . target . value ) }
310+ className = "border rounded px-2 py-1 text-sm"
311+ />
312+ < button
313+ onClick = { ( ) => updateComment ( app . id ) }
314+ className = "text-xs bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600"
315+ >
316+ Save
317+ </ button >
318+ < button
319+ onClick = { ( ) => {
320+ setEditingId ( null )
321+ setEditComment ( '' )
322+ } }
323+ className = "text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600"
324+ >
325+ Cancel
326+ </ button >
327+ </ div >
328+ ) : (
329+ < div className = "flex items-center space-x-2" >
330+ < span className = "text-sm text-gray-900" > { app . comment || '-' } </ span >
331+ < button
332+ onClick = { ( ) => {
333+ setEditingId ( app . id )
334+ setEditComment ( app . comment )
335+ } }
336+ className = "text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded hover:bg-gray-200"
337+ >
338+ Edit
339+ </ button >
340+ </ div >
341+ ) }
342+ </ td >
343+ < td className = "px-6 py-4" >
344+ < div className = "flex items-center justify-center" >
345+ < input
346+ type = "checkbox"
347+ checked = { app . certificate }
348+ onChange = { async ( ) => {
349+ try {
350+ const { error } = await supabase
351+ . from ( 'camp1_applications' )
352+ . update ( { certificate : ! app . certificate } )
353+ . eq ( 'id' , app . id )
354+ if ( error ) throw error
355+ fetchApplications ( )
356+ } catch ( error ) {
357+ console . error ( 'Error updating certificate status:' , error )
358+ alert ( 'Error updating certificate status' )
359+ }
360+ } }
361+ className = "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
362+ />
363+ </ div >
364+ </ td >
365+ < td className = "px-6 py-4" >
366+ < button
367+ onClick = { ( ) => {
368+ alert ( `
369+ Questions and Answers:
370+ Q1: ${ app . question1 }
371+ Q2: ${ app . question2 }
372+ Q3: ${ app . question3 }
373+ ` )
374+ } }
375+ className = "text-xs bg-indigo-100 text-indigo-700 px-2 py-1 rounded hover:bg-indigo-200"
376+ >
377+ View Details
378+ </ button >
379+ </ td >
380+ </ tr >
381+ ) ) }
382+ </ tbody >
383+ </ table >
384+ </ div >
385+ </ div >
386+ </ div >
387+ </ div >
388+ )
389+ }
0 commit comments