feat: implement cover image upload with canvas compression and db storage in RIT-EMS-main
This commit is contained in:
@@ -267,10 +267,69 @@ const App: React.FC = () => {
|
||||
profile = externalStudent;
|
||||
finalRole = 'STUDENT';
|
||||
} else {
|
||||
// FALLBACK: Auto-sync profile from metadata
|
||||
// FALLBACK: Auto-sync profile from metadata or auto-register from Google email format
|
||||
const meta = user.user_metadata;
|
||||
// ... (rest of metadata sync logic)
|
||||
if (meta && (meta.role === 'STUDENT' || meta.regNo)) {
|
||||
const emailLower = (user.email || '').trim().toLowerCase();
|
||||
const studentMatch = emailLower.match(/^student\.(\d{6})@([a-zA-Z0-9&-_]+)\.ritchennai\.edu\.in$/);
|
||||
|
||||
if (studentMatch) {
|
||||
const rollNo = studentMatch[1];
|
||||
const rawDept = studentMatch[2].toLowerCase();
|
||||
|
||||
// Calculate joining year and academic year (current local time is 2026-06-19)
|
||||
const joinYear = 2000 + parseInt(rollNo.substring(0, 2));
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth();
|
||||
const academicYearOffset = currentMonth >= 5 ? 1 : 0;
|
||||
const yearIndex = currentYear - joinYear + academicYearOffset;
|
||||
const years = ["1st Year", "2nd Year", "3rd Year", "4th Year"];
|
||||
const calculatedYear = years[yearIndex - 1] || "N/A";
|
||||
|
||||
// Map department
|
||||
let dept = rawDept.toUpperCase();
|
||||
if (dept === 'AIDS') dept = 'AI&DS';
|
||||
else if (dept === 'AIML') dept = 'AI&ML';
|
||||
else if (dept === 'VLSI') dept = 'EE(VLSI)';
|
||||
else if (dept === 'BIOTECH' || dept === 'BIO-TECH') dept = 'BIOTECH';
|
||||
else if (dept === 'H&S') dept = 'H&S Dept';
|
||||
|
||||
const syncData = {
|
||||
id: user.id,
|
||||
name: meta?.name || `Student ${rollNo}`,
|
||||
email: emailLower,
|
||||
reg_no: rollNo,
|
||||
phone: '',
|
||||
department: dept,
|
||||
year: calculatedYear,
|
||||
section: 'A',
|
||||
college_name: 'Rajalakshmi Institute of Technology',
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data: synced } = await supabase.from('Studentusers').upsert(syncData).select().single();
|
||||
profile = synced || syncData;
|
||||
finalRole = 'STUDENT';
|
||||
} else if (emailLower && !emailLower.endsWith('@ritchennai.edu.in') && !emailLower.endsWith('@rit.edu') && !HIGH_AUTH_ADMINS.includes(emailLower)) {
|
||||
// Auto-register external user if signing in via Google with a non-campus email
|
||||
const syncData = {
|
||||
id: user.id,
|
||||
name: meta?.name || emailLower.split('@')[0],
|
||||
email: emailLower,
|
||||
reg_no: 'EXT-' + Math.floor(Math.random() * 100000),
|
||||
phone: '',
|
||||
department: 'Others',
|
||||
year: 'N/A',
|
||||
section: 'N/A',
|
||||
college: 'External Institution',
|
||||
college_location: 'N/A',
|
||||
gender: 'Male',
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data: synced } = await supabase.from('externalusers').upsert(syncData).select().single();
|
||||
profile = synced || syncData;
|
||||
finalRole = 'STUDENT';
|
||||
} else if (meta && (meta.role === 'STUDENT' || meta.regNo)) {
|
||||
const isExternal = meta.signUpType === 'EXTERNAL';
|
||||
const table = isExternal ? 'externalusers' : 'Studentusers';
|
||||
|
||||
@@ -303,7 +362,7 @@ const App: React.FC = () => {
|
||||
finalRole = 'STUDENT';
|
||||
} else {
|
||||
console.warn("Auto-sync profile failed:", syncError?.message);
|
||||
profile = syncData; // Use metadata-based object as semi-functional fallback
|
||||
profile = syncData;
|
||||
finalRole = 'STUDENT';
|
||||
}
|
||||
} else if (meta && (meta.role === 'ADMIN' || meta.role === 'COORDINATOR')) {
|
||||
@@ -430,8 +489,36 @@ const App: React.FC = () => {
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
handleAuthChange(session);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [handleAuthChange]);
|
||||
|
||||
// 3. Real-time Database Subscriptions
|
||||
const eventsChannel = supabase
|
||||
.channel('events-sync')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'events' }, () => {
|
||||
fetchAllRegistrations();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
const specialEventsChannel = supabase
|
||||
.channel('special-events-sync')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'special_events' }, () => {
|
||||
fetchSpecialEvents();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
const announcementsChannel = supabase
|
||||
.channel('announcements-sync')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'announcements' }, () => {
|
||||
fetchAnnouncements();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
supabase.removeChannel(eventsChannel);
|
||||
supabase.removeChannel(specialEventsChannel);
|
||||
supabase.removeChannel(announcementsChannel);
|
||||
};
|
||||
}, [handleAuthChange, fetchAllRegistrations, fetchSpecialEvents, fetchAnnouncements]);
|
||||
|
||||
// NOTE: CreateEventForm already handles the Supabase insert.
|
||||
// This callback only refreshes the local events list.
|
||||
|
||||
@@ -196,7 +196,30 @@ const CreateEventForm: React.FC<CreateEventFormProps> = ({ onCancel, onSuccess,
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setFormData(prev => ({ ...prev, image: reader.result as string }));
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const max_width = 800;
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > max_width) {
|
||||
height = Math.round((height * max_width) / width);
|
||||
width = max_width;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const compressedBase64 = canvas.toDataURL('image/jpeg', 0.7);
|
||||
setFormData(prev => ({ ...prev, image: compressedBase64 }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, image: reader.result as string }));
|
||||
}
|
||||
};
|
||||
img.src = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
@@ -326,12 +349,8 @@ const CreateEventForm: React.FC<CreateEventFormProps> = ({ onCancel, onSuccess,
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let finalImageUrl = formData.image;
|
||||
|
||||
if (formData.image.startsWith('data:')) {
|
||||
const fileName = `events/${Date.now()}_${formData.title.replace(/\s+/g, '_')}.jpg`;
|
||||
finalImageUrl = await uploadToSupabase(formData.image, fileName);
|
||||
}
|
||||
// Keep Base64 string directly in the database, ignoring Firebase/Supabase storage
|
||||
const finalImageUrl = formData.image;
|
||||
|
||||
const totalLimit = parseInt(formData.maxParticipants);
|
||||
|
||||
|
||||
@@ -23,6 +23,17 @@ interface LoginFormProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const GoogleIcon = () => (
|
||||
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<path d="M21.35,11.1H12v2.7h5.38c-0.24,1.28 -0.96,2.37 -2.04,3.1v2.58h3.3c1.93,-1.78 3.04,-4.4 3.04,-7.48C21.68,11.96 21.56,11.49 21.35,11.1z" fill="#4285F4" />
|
||||
<path d="M12,20.58c2.43,0 4.47,-0.8 5.96,-2.2l-2.58,-2c-0.72,0.48 -1.64,0.77 -2.58,0.77 -2.37,0 -4.38,-1.6 -5.1,-3.75H4.31v2.1a8.4,8.4 0 0,0 7.69,5.08z" fill="#34A853" />
|
||||
<path d="M6.9,13.4c-0.18,-0.54 -0.29,-1.11 -0.29,-1.7 0,-0.59 0.11,-1.16 0.29,-1.7V7.9H4.31A8.4,8.4 0 0,0 3.3,11.7c0,1.38 0.33,2.69 1.01,3.8l2.59,-2.1z" fill="#FBBC05" />
|
||||
<path d="M12,6.85c1.32,0 2.5,0.45 3.44,1.35l2.58,-2.58C16.46,4.1 14.43,3.32 12,3.32c-4.79,0 -8.7,2.82 -10.39,6.9l2.59,2.1c0.72,-2.15 2.73,-3.75 5.1,-3.75z" fill="#EA4335" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
|
||||
@@ -55,6 +66,25 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
setErrorMessage(null);
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'google' });
|
||||
if (error) throw error;
|
||||
if (data?.user) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Google Auth error:", err);
|
||||
if (err.code !== 'auth/popup-closed-by-user') {
|
||||
setErrorMessage(err.message || "Google Sign-In failed.");
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
@@ -195,7 +225,7 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
/>
|
||||
{!signUpType && role === 'STUDENT' ? (
|
||||
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Portal</span></h2>
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className={RIT_BLUE_TEXT}>Portal</span></h2>
|
||||
<div className="grid gap-4">
|
||||
<button
|
||||
onClick={() => setSignUpType('INTERNAL')}
|
||||
@@ -205,20 +235,20 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSignUpType('EXTERNAL')}
|
||||
className="w-full py-6 bg-white border-2 border-slate-200 text-slate-700 rounded-2xl font-black uppercase text-xs tracking-[0.2em] transition-all hover:border-orange-500 hover:text-orange-500 hover:scale-[1.02] active:scale-95"
|
||||
className={`w-full py-6 bg-white border-2 border-slate-200 text-slate-700 rounded-2xl font-black uppercase text-xs tracking-[0.2em] transition-all hover:${RIT_BLUE_BORDER} hover:${RIT_BLUE_TEXT} hover:scale-[1.02] active:scale-95`}
|
||||
>
|
||||
Other College (External)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 overflow-y-auto no-scrollbar py-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 overflow-y-auto pr-3 py-4 scrollbar-hide">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
|
||||
{role === 'STUDENT' ? (signUpType === 'INTERNAL' ? 'Internal' : 'External') : (role === 'COORDINATOR' ? 'Event Coordinator' : 'Admin')} <span className="text-orange-500">Sign Up</span>
|
||||
{role === 'STUDENT' ? (signUpType === 'INTERNAL' ? 'Internal' : 'External') : (role === 'COORDINATOR' ? 'Event Coordinator' : 'Admin')} <span className={role === 'STUDENT' ? RIT_BLUE_TEXT : 'text-orange-500'}>Sign Up</span>
|
||||
</h2>
|
||||
{role === 'STUDENT' && <button type="button" onClick={() => setSignUpType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>}
|
||||
{role === 'STUDENT' && <button type="button" onClick={() => setSignUpType(null)} className={`text-[10px] font-black text-slate-400 uppercase hover:${RIT_BLUE_TEXT}`}>Change</button>}
|
||||
</div>
|
||||
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
||||
</div>
|
||||
@@ -278,7 +308,7 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
|
||||
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className="w-full py-5 bg-orange-500 text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl hover:bg-orange-600 transition-all shadow-lg shadow-orange-500/20 active:scale-95">
|
||||
<button type="submit" disabled={isSubmitting} className={`w-full py-5 ${role === 'STUDENT' ? `${RIT_BLUE} hover:bg-[#003366] shadow-blue-900/20` : 'bg-orange-500 hover:bg-orange-600 shadow-orange-500/20'} text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl transition-all shadow-lg active:scale-95`}>
|
||||
{isSubmitting ? 'Initializing...' : 'Sign Up Now'}
|
||||
</button>
|
||||
</form>
|
||||
@@ -354,6 +384,24 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||
<button type="submit" disabled={isSubmitting} className={`w-full py-5 ${RIT_BLUE} text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl ${RIT_BLUE_HOVER} transition-all shadow-lg shadow-blue-900/20 active:scale-95`}>
|
||||
{isSubmitting ? 'Verifying...' : 'Sign In Now'}
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-full flex items-center justify-center gap-4">
|
||||
<div className="h-px bg-slate-100 flex-1" />
|
||||
<span className="text-[9px] font-black text-slate-300 uppercase tracking-widest">or use Google</span>
|
||||
<div className="h-px bg-slate-100 flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Google Sign In Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-4.5 px-6 bg-white border border-slate-200 hover:border-slate-300 rounded-2xl flex items-center justify-center text-[10px] font-black text-slate-700 uppercase tracking-[0.2em] hover:bg-slate-50 transition-all hover:scale-[1.02] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
<GoogleIcon />
|
||||
Sign in with Google
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
createUserWithEmailAndPassword,
|
||||
signOut,
|
||||
onAuthStateChanged,
|
||||
getUser
|
||||
getUser,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup
|
||||
} from 'firebase/auth';
|
||||
import {
|
||||
getFirestore,
|
||||
@@ -318,6 +320,27 @@ class FirestoreQueryBuilder {
|
||||
|
||||
// Map supabase.auth
|
||||
const supabaseAuth = {
|
||||
async signInWithOAuth({ provider }: { provider: string }) {
|
||||
if (provider !== 'google') {
|
||||
return { data: { provider: null }, error: new Error("Unsupported OAuth provider") };
|
||||
}
|
||||
try {
|
||||
const googleProvider = new GoogleAuthProvider();
|
||||
const credential = await signInWithPopup(auth, googleProvider);
|
||||
const user = {
|
||||
id: credential.user.uid,
|
||||
email: credential.user.email,
|
||||
user_metadata: {
|
||||
name: credential.user.displayName
|
||||
}
|
||||
};
|
||||
return { data: { user }, error: null };
|
||||
} catch (error: any) {
|
||||
console.error("Firebase Google Auth Signin Error:", error);
|
||||
return { data: { user: null }, error };
|
||||
}
|
||||
},
|
||||
|
||||
async signInWithPassword({ email, password }: any) {
|
||||
try {
|
||||
const credential = await signInWithEmailAndPassword(auth, email, password);
|
||||
|
||||
Reference in New Issue
Block a user