Convert backends to Firebase and combine projects
This commit is contained in:
591
RIT-EVENT-MANAGEMENT--main/components/StatusTrackerView.tsx
Normal file
591
RIT-EVENT-MANAGEMENT--main/components/StatusTrackerView.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { uploadToSupabase, supabase } from '../supabase';
|
||||
|
||||
|
||||
interface StatusTrackerViewProps {
|
||||
event: Event;
|
||||
registration?: any;
|
||||
onBack: () => void;
|
||||
onUploadCertificate: (data: string) => Promise<string>;
|
||||
onShowCreateTeam?: () => void;
|
||||
onShowJoinTeam?: () => void;
|
||||
}
|
||||
|
||||
const SuccessPopup: React.FC<{ onClose: () => void }> = ({ onClose }) => (
|
||||
<div className="fixed inset-0 z-[10005] flex items-center justify-center p-6 bg-black/80 backdrop-blur-xl animate-in fade-in duration-300">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-sm p-12 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
|
||||
<div className="w-24 h-24 bg-emerald-100 text-emerald-500 rounded-full flex items-center justify-center text-4xl mb-8 shadow-inner">
|
||||
<i className="fas fa-cloud-check"></i>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-2 uppercase tracking-tight">File Captured</h3>
|
||||
<p className="text-gray-500 font-bold text-[10px] uppercase tracking-[0.3em] mb-10">Proof uploaded successfully</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-5 bg-emerald-600 text-white rounded-2xl font-black uppercase tracking-widest text-[11px] hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-200 active:scale-95"
|
||||
>
|
||||
Track Verification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
const StatusTrackerView: React.FC<StatusTrackerViewProps> = ({ event, registration, onBack, onUploadCertificate, onShowCreateTeam, onShowJoinTeam }) => {
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [now, setNow] = useState(new Date());
|
||||
const [attendanceRecords, setAttendanceRecords] = useState<any[]>([]);
|
||||
|
||||
const fetchAttendance = async () => {
|
||||
if (!registration?.id) return;
|
||||
const { data, error } = await supabase
|
||||
.from('attendance_records')
|
||||
.select('*')
|
||||
.eq('registration_id', registration.id);
|
||||
if (!error && data) {
|
||||
setAttendanceRecords(data);
|
||||
}
|
||||
};
|
||||
|
||||
const odUrl = registration?.od_url || registration?.od;
|
||||
const certUrl = registration?.certification_url || registration?.certifications;
|
||||
const certStatus = registration?.certification_status || registration?.certification_approval;
|
||||
|
||||
const [liveOdUrl, setLiveOdUrl] = useState(odUrl);
|
||||
const [liveCertUrl, setLiveCertUrl] = useState(certUrl);
|
||||
const [liveCertStatus, setLiveCertStatus] = useState(certStatus);
|
||||
|
||||
useEffect(() => {
|
||||
// Dynamic refetch to catch real-time faculty OD uploads without full page reload
|
||||
if (registration?.id) {
|
||||
supabase.from('registrations').select('*').eq('id', registration.id).single().then(({ data }) => {
|
||||
if (data) {
|
||||
setLiveOdUrl(data.od_url || data.od);
|
||||
setLiveCertUrl(data.certification_url || data.certifications);
|
||||
setLiveCertStatus(data.certification_status || data.certification_approval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (registration?.id) {
|
||||
fetchAttendance();
|
||||
|
||||
// Real-time subscription for attendance
|
||||
const channel = supabase
|
||||
.channel(`attendance-${registration.id}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'attendance_records',
|
||||
filter: `registration_id=eq.${registration.id}`
|
||||
},
|
||||
() => {
|
||||
fetchAttendance();
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}
|
||||
const timer = setInterval(() => setNow(new Date()), 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, [registration?.id, event.id]);
|
||||
|
||||
const isEventEnded = event.status === 'Completed';
|
||||
|
||||
const progressSteps = useMemo(() => {
|
||||
const isFree = event.pricingType === 'FREE';
|
||||
const isPaidVerified = registration?.payment_status === 'COMPLETED';
|
||||
const isManuallyEnded = event.status === 'Completed';
|
||||
const isManuallyOngoing = event.status === 'Event Ongoing';
|
||||
|
||||
const isEnded = isManuallyEnded;
|
||||
const isOngoing = isManuallyOngoing;
|
||||
|
||||
const hasUploaded = !!liveCertUrl;
|
||||
const isApproved = liveCertStatus === 'APPROVED';
|
||||
const hasUploadedOd = !!liveOdUrl;
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Registered',
|
||||
status: 'completed',
|
||||
icon: 'fa-user-check',
|
||||
color: 'bg-emerald-500',
|
||||
detail: 'Identity Secured'
|
||||
},
|
||||
{
|
||||
label: 'TEAM',
|
||||
status: !event.isTeamEvent || registration?.team_code ? 'completed' : 'active',
|
||||
icon: 'fa-users',
|
||||
color: 'bg-orange-500',
|
||||
detail: !event.isTeamEvent ? 'Solo Mode' : (registration?.team_code ? `Team: ${registration.team_name || 'Joined'}` : 'Wait for Team')
|
||||
},
|
||||
{
|
||||
label: 'Payment',
|
||||
status: isFree || isPaidVerified ? 'completed' : 'active',
|
||||
icon: 'fa-credit-card',
|
||||
color: 'bg-blue-400',
|
||||
detail: isFree ? 'Waiver Applied' : (isPaidVerified ? 'Funds Verified' : 'Awaiting Payment')
|
||||
},
|
||||
{
|
||||
label: 'Ticket',
|
||||
status: isFree || isPaidVerified ? 'completed' : 'pending',
|
||||
icon: 'fa-ticket-alt',
|
||||
color: 'bg-amber-500',
|
||||
detail: isFree || isPaidVerified ? 'Access Granted' : 'Locked'
|
||||
},
|
||||
{
|
||||
label: 'Ongoing',
|
||||
status: isEnded ? 'completed' : (isOngoing ? 'active' : 'pending'),
|
||||
icon: 'fa-play-circle',
|
||||
color: 'bg-purple-500',
|
||||
detail: isOngoing ? 'Live Session' : (isEnded ? 'Session Ended' : 'Scheduled')
|
||||
},
|
||||
{
|
||||
label: 'Ended',
|
||||
status: isEnded ? 'completed' : 'pending',
|
||||
icon: 'fa-calendar-check',
|
||||
color: 'bg-rose-500',
|
||||
detail: isEnded ? 'Archived' : 'Wait for Admin'
|
||||
},
|
||||
{
|
||||
label: 'Certification',
|
||||
status: isApproved ? 'completed' : (isEnded ? 'active' : 'pending'),
|
||||
icon: isApproved ? 'fa-check-double' : (hasUploaded ? 'fa-spinner fa-spin' : 'fa-award'),
|
||||
color: isApproved ? 'bg-emerald-600' : (isEnded ? 'bg-amber-500' : 'bg-indigo-600'),
|
||||
detail: isApproved ? 'Verified by Faculty' : (hasUploaded ? 'In Review' : (isEnded ? 'Upload Proof' : 'Wait for End'))
|
||||
},
|
||||
{
|
||||
label: 'OD',
|
||||
status: hasUploadedOd ? 'completed' : (isApproved ? 'active' : 'pending'),
|
||||
icon: 'fa-file-signature',
|
||||
color: 'bg-teal-500',
|
||||
detail: hasUploadedOd ? 'OD Provided' : (isApproved ? 'Ready for Download' : 'Wait for Approval')
|
||||
},
|
||||
];
|
||||
}, [event, registration, now, liveOdUrl, liveCertUrl, liveCertStatus]);
|
||||
|
||||
const handleOdDownload = async () => {
|
||||
if (!liveOdUrl) return;
|
||||
try {
|
||||
const response = await fetch(liveOdUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
const isPdf = liveOdUrl.toLowerCase().endsWith('.pdf') || blob.type === 'application/pdf';
|
||||
const extension = isPdf ? 'pdf' : 'jpg';
|
||||
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = `OD_${event.title.replace(/\s+/g, '_')}.${extension}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (err) {
|
||||
console.error("OD Download failed:", err);
|
||||
// Fallback: just open the URL directly if local fetch somehow fails
|
||||
window.open(liveOdUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (file && user && registration) {
|
||||
setIsUploading(true);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64 = reader.result as string;
|
||||
const fileName = `Cert_${user.id}_${event.id}_${Date.now()}.jpg`;
|
||||
|
||||
// STORE IN 'Certifications' BUCKET AS REQUESTED
|
||||
const publicUrl = await uploadToSupabase(base64, fileName, 'Certifications');
|
||||
|
||||
await supabase.from('registrations').update({
|
||||
certification_url: publicUrl,
|
||||
certification_status: 'PENDING_APPROVAL'
|
||||
}).eq('id', registration.id);
|
||||
|
||||
setShowSuccess(true);
|
||||
} catch (err) {
|
||||
console.error("Upload failed:", err);
|
||||
alert("Upload failed. Please check your connection or bucket permissions.");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-40 pb-16 px-6 md:px-12 lg:px-24 bg-[#F3F4F6] min-h-screen animate-in fade-in duration-500 font-inter">
|
||||
{showSuccess && <SuccessPopup onClose={() => { setShowSuccess(false); window.location.reload(); }} />}
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-16">
|
||||
<div className="flex flex-col items-start">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-[#f97316] font-black uppercase tracking-[0.2em] mb-6 text-[10px] group">
|
||||
<i className="fas fa-arrow-left transition-transform group-hover:-translate-x-1"></i> Return to Hub
|
||||
</button>
|
||||
<h1 className="text-6xl font-black text-[#1A202C] tracking-tighter mb-2 leading-none uppercase">REAL-TIME <span className="text-[#f97316]">PROGRESS</span></h1>
|
||||
<p className="text-gray-400 font-bold uppercase tracking-[0.3em] text-xs">Tracking Node: {event.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-[3.5rem] p-8 md:p-16 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 relative overflow-hidden">
|
||||
<div className="relative z-10 w-full overflow-x-auto no-scrollbar pb-6">
|
||||
<div className="min-w-[800px] relative">
|
||||
|
||||
{/* Continuous Line Background */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
|
||||
style={{ left: `${100 / (progressSteps.length * 2)}%`, right: `${100 / (progressSteps.length * 2)}%` }}
|
||||
></div>
|
||||
|
||||
{/* Active Line Foreground */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gradient-to-r from-emerald-400 to-orange-400 rounded-full z-0 transition-all duration-1000"
|
||||
style={{
|
||||
left: `${100 / (progressSteps.length * 2)}%`,
|
||||
width: `calc(${
|
||||
(progressSteps.findIndex(s => s.status === 'active') !== -1
|
||||
? progressSteps.findIndex(s => s.status === 'active')
|
||||
: Math.max(0, progressSteps.filter(s => s.status === 'completed').length - 1))
|
||||
/ (progressSteps.length - 1)
|
||||
} * (100% - ${100 / progressSteps.length}%))`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Steps Container */}
|
||||
<div className="flex w-full justify-between items-start relative z-10">
|
||||
{progressSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
|
||||
|
||||
{/* Dot */}
|
||||
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
|
||||
step.status === 'completed' ? 'bg-emerald-500' :
|
||||
step.status === 'active' ? 'bg-orange-500 ring-4 ring-orange-100' :
|
||||
'bg-gray-200'
|
||||
}`}></div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
|
||||
step.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100' :
|
||||
step.status === 'active' ? 'bg-white border-2 border-orange-500 text-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)] scale-110 group-hover:scale-125' :
|
||||
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500'
|
||||
}`}>
|
||||
<i className={`fas ${step.icon} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
|
||||
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
|
||||
step.status === 'active' ? 'text-orange-500' :
|
||||
step.status === 'completed' ? 'text-gray-700 group-hover:text-emerald-600' :
|
||||
'text-gray-400 group-hover:text-gray-600'
|
||||
}`}>{step.label}</span>
|
||||
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">{step.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Attendance Tracker Redesign (Node-based) */}
|
||||
<div className="mt-20 pt-12 border-t border-gray-100 flex flex-col items-center">
|
||||
<div className="flex items-center gap-3 mb-10">
|
||||
<div className="w-1.5 h-4 bg-[#f97316] rounded-full"></div>
|
||||
<span className="text-[11px] font-black text-slate-800 uppercase tracking-[0.2em]">Attendance Milestones</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-4xl relative">
|
||||
{(() => {
|
||||
const allSessions: { label: string, status: string, day: string, batch: string }[] = [];
|
||||
if (event.schedule && event.schedule.length > 0) {
|
||||
// Group by day_idx
|
||||
const dayMap = new Map<number, typeof event.schedule>();
|
||||
event.schedule.forEach(s => {
|
||||
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
|
||||
dayMap.get(s.day_idx)!.push(s);
|
||||
});
|
||||
const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]);
|
||||
sortedDays.forEach(([dayIdx, slots]) => {
|
||||
slots!.forEach((slot) => {
|
||||
const dLabel = `Day ${dayIdx}`;
|
||||
const bLabel = `Batch ${slot.batch_idx}`;
|
||||
const record = attendanceRecords.find(r => r.day_label === dLabel && r.batch_label === bLabel);
|
||||
allSessions.push({
|
||||
label: `D${dayIdx} B${slot.batch_idx}`,
|
||||
day: dLabel,
|
||||
batch: bLabel,
|
||||
status: record?.is_present ? 'completed' : (record?.is_present === false ? 'absent' : 'pending')
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const record = attendanceRecords.find(r => r.day_label === 'Day 1');
|
||||
allSessions.push({
|
||||
label: 'Day 1',
|
||||
day: 'Day 1',
|
||||
batch: '',
|
||||
status: record?.is_present ? 'completed' : (record?.is_present === false ? 'absent' : 'pending')
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Connecting Line Background */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-gray-100 rounded-full z-0"
|
||||
style={{ left: `${100 / (allSessions.length * 2)}%`, right: `${100 / (allSessions.length * 2)}%` }}
|
||||
></div>
|
||||
|
||||
{/* Connecting Line Foreground */}
|
||||
<div
|
||||
className="absolute top-2.5 h-1.5 bg-emerald-500 rounded-full z-0 transition-all duration-1000"
|
||||
style={{
|
||||
left: `${100 / (allSessions.length * 2)}%`,
|
||||
width: `calc(${
|
||||
(allSessions.findIndex(s => s.status === 'pending') !== -1
|
||||
? Math.max(0, allSessions.findIndex(s => s.status === 'pending') - 1)
|
||||
: Math.max(0, allSessions.filter(s => s.status === 'completed').length - 1))
|
||||
/ Math.max(1, allSessions.length - 1)
|
||||
} * (100% - ${100 / allSessions.length}%))`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Nodes */}
|
||||
<div className="flex w-full justify-between items-start relative z-10">
|
||||
{allSessions.map((session, sIdx) => (
|
||||
<div key={sIdx} className="flex flex-col items-center flex-1 relative group cursor-pointer">
|
||||
|
||||
{/* Dot on line */}
|
||||
<div className={`w-6 h-6 rounded-full border-4 border-white shadow-sm mb-10 transition-all duration-500 group-hover:scale-125 ${
|
||||
session.status === 'completed' ? 'bg-emerald-500' :
|
||||
session.status === 'absent' ? 'bg-rose-500 animate-pulse' :
|
||||
'bg-gray-200'
|
||||
}`}></div>
|
||||
|
||||
{/* Large Circle Shaped Indicator */}
|
||||
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full flex items-center justify-center transition-all duration-500 mb-6 group-hover:-translate-y-2 group-hover:shadow-lg ${
|
||||
session.status === 'completed' ? 'bg-emerald-50 text-emerald-500 group-hover:bg-emerald-100 border-2 border-emerald-500/20' :
|
||||
session.status === 'absent' ? 'bg-rose-50 text-rose-500 group-hover:bg-rose-100 border-2 border-rose-500/20' :
|
||||
'bg-gray-50 text-gray-300 group-hover:bg-gray-100 group-hover:text-gray-500 border-2 border-slate-100'
|
||||
}`}>
|
||||
<i className={`fas ${
|
||||
session.status === 'completed' ? 'fa-calendar-check' :
|
||||
session.status === 'absent' ? 'fa-calendar-times' :
|
||||
'fa-calendar'
|
||||
} text-xl md:text-2xl transition-transform duration-300 group-hover:scale-110`}></i>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="text-center transition-transform duration-300 group-hover:translate-y-1">
|
||||
<span className={`block text-[10px] md:text-xs font-black uppercase tracking-[0.15em] mb-1.5 transition-colors duration-300 ${
|
||||
session.status === 'completed' ? 'text-[#1A202C]' :
|
||||
session.status === 'absent' ? 'text-rose-500' :
|
||||
'text-gray-400 group-hover:text-gray-600'
|
||||
}`}>{session.label}</span>
|
||||
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest whitespace-nowrap">
|
||||
{session.status === 'completed' ? 'Attended' :
|
||||
session.status === 'absent' ? 'Absent' : 'Upcoming'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 bg-gray-50/50 px-8 py-3 rounded-2xl border border-gray-100">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<i className="fas fa-chart-line text-[#f97316]"></i>
|
||||
{attendanceRecords.filter(r => r.is_present).length} / {Math.max(1, event.schedule?.length || 1)} Sessions Attended
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Management Buttons - Relocated from RegistrationsView */}
|
||||
{event.isTeamEvent && !registration?.team_code && (
|
||||
<div className="mt-12 p-10 bg-white rounded-[3.5rem] shadow-xl shadow-gray-900/5 border border-gray-100 relative overflow-hidden group animate-in slide-in-from-bottom-5">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl group-hover:bg-orange-100 transition-colors"></div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
|
||||
<div className="text-center md:text-left">
|
||||
<div className="flex justify-center md:justify-start items-center gap-3 mb-3">
|
||||
<div className="w-2 h-5 bg-[#f97316] rounded-full"></div>
|
||||
<span className="text-xs font-black text-slate-900 uppercase tracking-widest">Team Management</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest max-w-md">This is a team event. Form your own team or join an existing alliance to participate in this competition.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||
<button
|
||||
onClick={onShowCreateTeam}
|
||||
className="flex-1 md:flex-none py-5 px-10 bg-white border-2 border-slate-100 text-slate-900 rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-sm hover:border-[#f97316] hover:text-[#f97316] transition-all active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-plus-circle"></i> Create Team
|
||||
</button>
|
||||
<button
|
||||
onClick={onShowJoinTeam}
|
||||
className="flex-1 md:flex-none py-5 px-10 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-xl shadow-gray-200 hover:bg-black transition-all active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<i className="fas fa-right-to-bracket"></i> Join Alliance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OD Document Download Section */}
|
||||
{liveOdUrl && (
|
||||
<div className="bg-white rounded-[3.5rem] p-8 md:p-12 shadow-xl shadow-gray-900/5 border border-gray-100 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 animate-in slide-in-from-bottom-5">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 bg-teal-50 text-teal-500 rounded-3xl flex items-center justify-center text-2xl shadow-inner">
|
||||
<i className="fas fa-file-signature"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-[#1A202C] tracking-tight uppercase">Official On-Duty Document</h3>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mt-1">Authorized by Faculty Coordinator</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 w-full md:w-auto">
|
||||
<a href={liveOdUrl} target="_blank" rel="noreferrer" className="flex-1 md:w-auto px-8 py-4 bg-gray-50 text-gray-600 rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-gray-100 transition-all border border-gray-200 text-center">
|
||||
<i className="fas fa-eye mr-2"></i> Inspect
|
||||
</a>
|
||||
<button onClick={handleOdDownload} className="flex-1 md:w-auto px-8 py-4 bg-teal-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest hover:bg-teal-700 transition-all shadow-xl shadow-teal-600/20 text-center">
|
||||
<i className="fas fa-download mr-2"></i> Download OD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 bg-white p-12 rounded-[3.5rem] shadow-xl shadow-gray-900/10 border border-gray-200 flex flex-col md:flex-row items-center gap-12">
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-[#1A202C] mb-4 uppercase tracking-tight">Certification Portal</h3>
|
||||
<p className="text-gray-500 font-medium mb-8 leading-relaxed max-w-sm">Provide valid proof of attendance or task completion to finalize your official event participation.</p>
|
||||
|
||||
<input type="file" ref={fileInputRef} className="hidden" onChange={handleFileChange} accept="image/*" />
|
||||
|
||||
{!isEventEnded ? (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<button
|
||||
disabled
|
||||
className="px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200 flex items-center gap-4"
|
||||
>
|
||||
<i className="fas fa-lock"></i> UPLOAD LOCKED
|
||||
</button>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-hourglass-half text-[#f97316]"></i>
|
||||
Upload unlocks once admin marks event as Ended
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading || liveCertStatus === 'APPROVED'}
|
||||
className={`px-12 py-5 rounded-2xl font-black text-xs uppercase tracking-[0.3em] shadow-2xl transition-all active:scale-95 flex items-center gap-4 ${liveCertStatus === 'APPROVED' ? 'bg-emerald-50 text-emerald-500 cursor-not-allowed border border-emerald-100' :
|
||||
isUploading ? 'bg-gray-200 text-gray-400 cursor-wait' : 'bg-[#1A202C] text-white hover:bg-black'
|
||||
}`}
|
||||
>
|
||||
{isUploading ? <><i className="fas fa-spinner fa-spin"></i> SYNCING...</> :
|
||||
liveCertStatus === 'APPROVED' ? <><i className="fas fa-check-double"></i> VERIFIED</> :
|
||||
liveCertUrl ? <><i className="fas fa-clock"></i> RE-UPLOAD PROOF</> :
|
||||
<><i className="fas fa-cloud-arrow-up"></i> UPLOAD PROOF</>}
|
||||
</button>
|
||||
{liveCertStatus === 'PENDING_APPROVAL' && (
|
||||
<p className="mt-4 text-[10px] font-black text-[#f97316] uppercase tracking-widest flex items-center gap-2">
|
||||
<i className="fas fa-info-circle"></i> Awaiting Faculty Review
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-64 aspect-square bg-gray-50 rounded-[2.5rem] border-2 border-dashed border-gray-200 flex items-center justify-center overflow-hidden group">
|
||||
{liveCertUrl ? (
|
||||
<img src={liveCertUrl} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" alt="Proof" />
|
||||
) : (
|
||||
<div className="text-center p-6">
|
||||
<i className="fas fa-image text-gray-200 text-4xl mb-4"></i>
|
||||
<p className="text-[10px] font-black text-gray-300 uppercase tracking-widest">Preview Area</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1A202C] p-12 rounded-[3.5rem] text-white shadow-2xl shadow-gray-900/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
|
||||
<h3 className="text-2xl font-black mb-10 tracking-tight uppercase flex items-center gap-4">
|
||||
Event Info
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
</h3>
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Conducting Club</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{event.club || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Enrollment Status</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{registration?.payment_status || 'PENDING'}</span>
|
||||
</div>
|
||||
{event.durationDays && (
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Duration</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">{event.durationDays} Days</span>
|
||||
</div>
|
||||
)}
|
||||
{event.schedule && event.schedule.length > 0 && (
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Sessions</span>
|
||||
<span className="text-white font-black text-[11px] uppercase">
|
||||
{event.schedule.length} Total
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Attendance Progress</span>
|
||||
<span className="text-emerald-400 font-black text-[11px] uppercase">
|
||||
{attendanceRecords.filter(r => r.is_present).length} Marked
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center border-b border-white/5 pb-4">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Certification</span>
|
||||
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-[#f97316]'}`}>
|
||||
{liveCertStatus?.replace('_', ' ') || 'NOT SUBMITTED'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/40 text-[10px] font-black uppercase tracking-widest">Verification Status</span>
|
||||
<span className={`font-black text-[11px] uppercase ${liveCertStatus === 'APPROVED' ? 'text-emerald-400' : 'text-white/60'}`}>
|
||||
{liveCertStatus === 'APPROVED' ? 'Finalized' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusTrackerView;
|
||||
Reference in New Issue
Block a user