Skip to content

Commit 452ca89

Browse files
committed
feat : dashboard
1 parent 2c3244a commit 452ca89

File tree

1 file changed

+389
-0
lines changed
  • src/app/dashboard/camp/[id]/camp-selection

1 file changed

+389
-0
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
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

Comments
 (0)