Convert backends to Firebase and combine projects
This commit is contained in:
420
RIT-EVENT-MANAGEMENT--main/components/ProfileView.tsx
Normal file
420
RIT-EVENT-MANAGEMENT--main/components/ProfileView.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user