Convert backends to Firebase and combine projects

This commit is contained in:
2026-06-18 14:07:24 +05:30
commit 0a76feafc5
147 changed files with 35104 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
import React, { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { UserRole } from '../types';
import { supabase, uploadToSupabase } from '../supabase';
const compressImage = (base64Str: string): Promise<string> => {
return new Promise((resolve) => {
const img = new Image();
img.src = base64Str;
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 400;
const MAX_HEIGHT = 400;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL('image/jpeg', 0.7));
};
});
};
interface ProfileViewProps {
onLogout?: () => void;
onSupabaseError?: () => void;
}
const ProfileView: React.FC<ProfileViewProps> = ({ onLogout, onSupabaseError }) => {
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [userData, setUserData] = useState<any>(null);
const [userRole, setUserRole] = useState<UserRole>('STUDENT');
const [userTable, setUserTable] = useState<string>('Studentusers');
const [regCount, setRegCount] = useState(0);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState({ name: '', phone: '', profile_photo: '', year: '', section: '' });
const [showSuccess, setShowSuccess] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [pastEvents, setPastEvents] = useState<any[]>([]);
const fetchProfile = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
try {
let profile = null;
let role: UserRole = 'STUDENT';
let table = 'Studentusers';
const HIGH_AUTH_ADMINS = [
'adrit1highauth@gmail.com',
'adrit2highauth@gmail.com',
'adrit3highauth@gmail.com',
'adrit4highauth@gmail.com',
'adrit5highauth@gmail.com'
];
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
const { data: faculty } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
if (faculty) { profile = faculty; role = 'ADMIN'; table = 'Facultyusers'; }
} else {
const [studentRes, extRes, adminRes, facultyRes] = await Promise.all([
supabase.from('Studentusers').select('*').eq('id', user.id).single(),
supabase.from('externalusers').select('*').eq('id', user.id).single(),
supabase.from('Adminusers').select('*').eq('id', user.id).single(),
supabase.from('Facultyusers').select('*').eq('id', user.id).single()
]);
if (studentRes.data) { profile = studentRes.data; role = 'STUDENT'; table = 'Studentusers'; }
else if (extRes.data) { profile = extRes.data; role = 'STUDENT'; table = 'externalusers'; }
else if (adminRes.data) { profile = adminRes.data; role = 'COORDINATOR'; table = 'Adminusers'; }
else if (facultyRes.data) { profile = facultyRes.data; role = 'COORDINATOR'; table = 'Facultyusers'; }
}
if (profile) {
setUserData(profile);
setUserRole(role);
setUserTable(table);
setEditForm({ name: profile.name || '', phone: profile.phone || '', profile_photo: profile.profile_photo || '', year: profile.year || '', section: profile.section || '' });
}
// Fetch registration count & participation history concurrently
const [countRes, historyRes] = await Promise.all([
supabase.from('registrations').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
supabase.from('participants').select('*, events(title, category)').eq('user_id', user.id).order('participation_date', { ascending: false })
]);
setRegCount(countRes.count || 0);
if (historyRes.data) setPastEvents(historyRes.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchProfile(); }, []);
const handleUpdate = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
setIsSaving(true);
setErrorMessage(null);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
let photoUrl = editForm.profile_photo;
if (photoUrl.startsWith('data:')) {
photoUrl = await uploadToSupabase(photoUrl, `avatars/${user.id}_${Date.now()}.jpg`, 'Profile');
}
const { error } = await supabase.from(userTable).update({
name: editForm.name,
phone: editForm.phone,
profile_photo: photoUrl,
year: editForm.year,
section: editForm.section
}).eq('id', user.id);
if (error) throw error;
setUserData({ ...userData, name: editForm.name, phone: editForm.phone, profile_photo: photoUrl, year: editForm.year, section: editForm.section });
setIsEditing(false);
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
} catch (err: any) {
setErrorMessage(err.message);
if (err.message?.includes('RLS')) onSupabaseError?.();
} finally {
setIsSaving(false);
}
};
const handlePhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = async () => {
const compressed = await compressImage(reader.result as string);
setEditForm({ ...editForm, profile_photo: compressed });
};
reader.readAsDataURL(file);
}
};
if (loading) return <div className="h-screen flex items-center justify-center bg-[#F3F4F6]"><i className="fas fa-spinner fa-spin text-4xl text-[#f97316]"></i></div>;
const avatarUrl = isEditing ? (editForm.profile_photo || userData?.profile_photo) : userData?.profile_photo;
return (
<div className="min-h-screen bg-gray-50 pt-32 pb-12 px-4 sm:px-6 lg:px-8 font-inter">
{showSuccess && createPortal(
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-6 bg-black/40 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-2xl w-full max-w-sm p-8 shadow-2xl flex flex-col items-center text-center transform transition-all scale-100">
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center text-2xl mb-6">
<i className="fas fa-check"></i>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Profile Updated</h3>
<p className="text-gray-500 mb-6">Your changes have been saved successfully.</p>
<button onClick={() => setShowSuccess(false)} className="w-full py-3 bg-gray-900 text-white rounded-xl font-semibold hover:bg-gray-800 transition-colors">
Continue
</button>
</div>
</div>, document.body
)}
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column: Profile Card */}
<div className="lg:col-span-1">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden sticky top-32">
<div className="h-32 bg-gradient-to-r from-orange-400 to-rose-500"></div>
<div className="px-6 pb-8">
<div className="relative -mt-16 mb-6 flex justify-center">
<div className="w-32 h-32 rounded-full border-4 border-white shadow-lg overflow-hidden bg-gray-100 relative group">
<img
src={avatarUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${userData?.name}`}
alt="Profile"
className="w-full h-full object-cover"
/>
{isEditing && (
<div
onClick={() => fileInputRef.current?.click()}
className="absolute inset-0 bg-black/50 flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
>
<i className="fas fa-camera text-white text-xl"></i>
</div>
)}
</div>
<input type="file" ref={fileInputRef} className="hidden" onChange={handlePhotoChange} accept="image/*" />
</div>
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-1">{userData?.name}</h2>
<p className="text-sm font-medium text-orange-500 uppercase tracking-wider mb-4">{userRole}</p>
<div className="flex items-center justify-center gap-2 text-gray-500 text-sm">
<i className="fas fa-envelope"></i>
<span>{userData?.email}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 border-t border-gray-100 pt-6 mb-6">
<div className="text-center">
<span className="block text-xl font-bold text-gray-900">{regCount}</span>
<span className="text-xs text-gray-500 uppercase tracking-wide">Registry</span>
</div>
<div className="text-center border-l border-gray-100">
<span className="block text-xl font-bold text-emerald-600">{pastEvents.length}</span>
<span className="text-xs text-gray-500 uppercase tracking-wide">Completed</span>
</div>
</div>
{!isEditing && (
<button
onClick={onLogout}
className="w-full py-2.5 border border-gray-200 text-gray-600 rounded-xl font-medium hover:bg-gray-50 hover:text-rose-600 transition-colors text-sm flex items-center justify-center gap-2"
>
<i className="fas fa-sign-out-alt"></i> Sign Out
</button>
)}
</div>
</div>
</div>
{/* Right Column: Details & Edit Form */}
<div className="lg:col-span-2 space-y-6">
{/* Personal Information Card */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="px-6 py-5 border-b border-gray-100 flex justify-between items-center">
<h3 className="text-lg font-bold text-gray-900">Personal Information</h3>
{!isEditing && userRole !== 'STUDENT' && (
<button
onClick={() => setIsEditing(true)}
className="text-sm font-medium text-orange-600 hover:text-orange-700 flex items-center gap-1"
>
<i className="fas fa-pen"></i> Edit
</button>
)}
</div>
<div className="p-6">
{isEditing ? (
<form onSubmit={handleUpdate} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Full Name</label>
<input
type="text"
value={editForm.name}
onChange={e => setEditForm({...editForm, name: e.target.value})}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
placeholder="Enter your full name"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Phone Number</label>
<input
type="tel"
value={editForm.phone}
onChange={e => setEditForm({...editForm, phone: e.target.value})}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
placeholder="Enter your phone number"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Year</label>
<select
value={editForm.year}
onChange={e => setEditForm({...editForm, year: e.target.value})}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
>
<option value="">Select Year</option>
<option value="I">I</option>
<option value="II">II</option>
<option value="III">III</option>
<option value="IV">IV</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Section</label>
<input
type="text"
value={editForm.section}
onChange={e => setEditForm({...editForm, section: e.target.value.toUpperCase()})}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all"
placeholder="e.g. A, B, C"
/>
</div>
</div>
{errorMessage && (
<div className="p-3 bg-rose-50 text-rose-600 text-sm rounded-lg flex items-center gap-2">
<i className="fas fa-exclamation-circle"></i>
{errorMessage}
</div>
)}
<div className="flex items-center gap-4 pt-4">
<button
type="submit"
disabled={isSaving}
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl font-medium hover:bg-black transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isSaving ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-save"></i>}
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
<button
type="button"
onClick={() => { setIsEditing(false); setErrorMessage(null); }}
className="px-6 py-2.5 bg-white border border-gray-200 text-gray-700 rounded-xl font-medium hover:bg-gray-50 transition-colors"
>
Cancel
</button>
</div>
</form>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-8 gap-x-12">
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Full Name</label>
<p className="text-base font-semibold text-gray-900">{userData?.name}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Email Address</label>
<p className="text-base font-semibold text-gray-900">{userData?.email}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Phone Number</label>
<p className="text-base font-semibold text-gray-900">{userData?.phone || 'Not provided'}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Role</label>
<p className="text-base font-semibold text-gray-900 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
{userRole}
</p>
</div>
{userData?.department && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Department</label>
<p className="text-base font-semibold text-gray-900">{userData.department}</p>
</div>
)}
{userData?.year && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Year</label>
<p className="text-base font-semibold text-gray-900">{userData.year}</p>
</div>
)}
{userData?.section && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Section</label>
<p className="text-base font-semibold text-gray-900">{userData.section}</p>
</div>
)}
{userData?.reg_no && (
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Registration Number</label>
<p className="text-base font-semibold text-gray-900">{userData.reg_no}</p>
</div>
)}
</div>
)}
</div>
</div>
{/* Additional Info / Activity Placeholder */}
{!isEditing && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="px-6 py-5 border-b border-gray-100">
<h3 className="text-lg font-bold text-gray-900">Recent Activity</h3>
</div>
<div className="p-6">
{pastEvents.length > 0 ? (
<div className="space-y-6">
{pastEvents.map((pe, idx) => (
<div key={idx} className="flex items-start gap-5 p-4 rounded-2xl hover:bg-gray-50 transition-colors border border-transparent hover:border-gray-100 group">
<div className="w-12 h-12 rounded-2xl bg-orange-50 text-orange-500 flex items-center justify-center text-lg shrink-0 shadow-sm border border-orange-100 group-hover:scale-110 transition-transform">
<i className="fas fa-calendar-check"></i>
</div>
<div>
<p className="text-sm font-black text-gray-900 uppercase tracking-tight">{pe.events?.title || 'Unknown Event'}</p>
<p className="text-[10px] text-gray-500 mt-1 uppercase font-bold tracking-widest">{pe.events?.category} Completed on {new Date(pe.participation_date).toLocaleDateString()}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-400">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-gray-100">
<i className="fas fa-history text-2xl opacity-20"></i>
</div>
<p className="text-xs font-black uppercase tracking-widest text-gray-400">No participation history yet</p>
<p className="text-[10px] text-gray-400 mt-2 font-medium">Events you participate in will appear here.</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default ProfileView;