feat: implement cover image upload with canvas compression and db storage in RIT-EMS-main
This commit is contained in:
@@ -43,8 +43,16 @@ public class AdminController {
|
|||||||
if (user.getRole() == null || user.getRole().trim().isEmpty()) {
|
if (user.getRole() == null || user.getRole().trim().isEmpty()) {
|
||||||
user.setRole("FACULTY");
|
user.setRole("FACULTY");
|
||||||
}
|
}
|
||||||
if (user.getDepartment() == null || user.getDepartment().trim().isEmpty()) {
|
|
||||||
user.setDepartment("H&S Dept");
|
String role = user.getRole();
|
||||||
|
if ("HOD".equals(role) || "FACULTY".equals(role)) {
|
||||||
|
if (user.getDepartment() == null || user.getDepartment().trim().isEmpty()) {
|
||||||
|
user.setDepartment("H&S Dept");
|
||||||
|
} else {
|
||||||
|
user.setDepartment(user.getDepartment().trim());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.setDepartment("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
|
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
|
||||||
@@ -61,7 +69,14 @@ public class AdminController {
|
|||||||
user.setFullName(userDetails.getFullName());
|
user.setFullName(userDetails.getFullName());
|
||||||
user.setEmail(userDetails.getEmail());
|
user.setEmail(userDetails.getEmail());
|
||||||
user.setRole(userDetails.getRole());
|
user.setRole(userDetails.getRole());
|
||||||
user.setDepartment(userDetails.getDepartment());
|
|
||||||
|
String role = user.getRole();
|
||||||
|
if ("HOD".equals(role) || "FACULTY".equals(role)) {
|
||||||
|
user.setDepartment(userDetails.getDepartment() != null ? userDetails.getDepartment().trim() : "");
|
||||||
|
} else {
|
||||||
|
user.setDepartment("");
|
||||||
|
}
|
||||||
|
|
||||||
user.setClubCoordinator(userDetails.isClubCoordinator());
|
user.setClubCoordinator(userDetails.isClubCoordinator());
|
||||||
user.setPlacementStaff(userDetails.isPlacementStaff());
|
user.setPlacementStaff(userDetails.isPlacementStaff());
|
||||||
user.setClassIncharge(userDetails.isClassIncharge());
|
user.setClassIncharge(userDetails.isClassIncharge());
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ public class EventController {
|
|||||||
if (payload.containsKey("isPublicEvent")) {
|
if (payload.containsKey("isPublicEvent")) {
|
||||||
event.setPublicEvent((boolean) payload.get("isPublicEvent"));
|
event.setPublicEvent((boolean) payload.get("isPublicEvent"));
|
||||||
}
|
}
|
||||||
|
if (payload.containsKey("image")) {
|
||||||
|
event.setImage(payload.get("image") != null ? payload.get("image").toString() : null);
|
||||||
|
}
|
||||||
|
|
||||||
// Workflow Logic
|
// Workflow Logic
|
||||||
if ("PRINCIPAL".equals(proposer.getRole())) {
|
if ("PRINCIPAL".equals(proposer.getRole())) {
|
||||||
@@ -202,6 +205,9 @@ public class EventController {
|
|||||||
if (payload.containsKey("requirements")) {
|
if (payload.containsKey("requirements")) {
|
||||||
event.setRequirements((List<String>) payload.get("requirements"));
|
event.setRequirements((List<String>) payload.get("requirements"));
|
||||||
}
|
}
|
||||||
|
if (payload.containsKey("image")) {
|
||||||
|
event.setImage(payload.get("image") != null ? payload.get("image").toString() : null);
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.get("proposer") != null) {
|
if (payload.get("proposer") != null) {
|
||||||
try {
|
try {
|
||||||
@@ -350,6 +356,9 @@ public class EventController {
|
|||||||
|
|
||||||
if (payload.containsKey("centreName")) event.setCentreName(payload.get("centreName").toString());
|
if (payload.containsKey("centreName")) event.setCentreName(payload.get("centreName").toString());
|
||||||
if (payload.containsKey("isPublicEvent")) event.setPublicEvent((boolean) payload.get("isPublicEvent"));
|
if (payload.containsKey("isPublicEvent")) event.setPublicEvent((boolean) payload.get("isPublicEvent"));
|
||||||
|
if (payload.containsKey("image")) {
|
||||||
|
event.setImage(payload.get("image") != null ? payload.get("image").toString() : null);
|
||||||
|
}
|
||||||
if (payload.containsKey("status") && isAdmin) {
|
if (payload.containsKey("status") && isAdmin) {
|
||||||
event.setStatus(payload.get("status").toString());
|
event.setStatus(payload.get("status").toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ public class Event extends BaseEntity {
|
|||||||
@Column(columnDefinition = "boolean default false")
|
@Column(columnDefinition = "boolean default false")
|
||||||
private boolean isPublicEvent;
|
private boolean isPublicEvent;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "LONGTEXT")
|
||||||
|
private String image;
|
||||||
|
|
||||||
@jakarta.persistence.Transient
|
@jakarta.persistence.Transient
|
||||||
private String conflictMessage;
|
private String conflictMessage;
|
||||||
|
|
||||||
@@ -169,6 +172,14 @@ public class Event extends BaseEntity {
|
|||||||
isPublicEvent = publicEvent;
|
isPublicEvent = publicEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImage() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImage(String image) {
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
|
||||||
// Manual Builder
|
// Manual Builder
|
||||||
public static EventBuilder builder() {
|
public static EventBuilder builder() {
|
||||||
return new EventBuilder();
|
return new EventBuilder();
|
||||||
@@ -198,6 +209,7 @@ public class Event extends BaseEntity {
|
|||||||
public EventBuilder sponsors(List<String> sponsors) { event.setSponsors(sponsors); return this; }
|
public EventBuilder sponsors(List<String> sponsors) { event.setSponsors(sponsors); return this; }
|
||||||
public EventBuilder proposer(User proposer) { event.setProposer(proposer); return this; }
|
public EventBuilder proposer(User proposer) { event.setProposer(proposer); return this; }
|
||||||
public EventBuilder groupRequestId(String groupRequestId) { event.setGroupRequestId(groupRequestId); return this; }
|
public EventBuilder groupRequestId(String groupRequestId) { event.setGroupRequestId(groupRequestId); return this; }
|
||||||
|
public EventBuilder image(String image) { event.setImage(image); return this; }
|
||||||
|
|
||||||
public Event build() {
|
public Event build() {
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ const AppContent: React.FC = () => {
|
|||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
let filtered = data;
|
let filtered = data;
|
||||||
if (user?.role === 'HOD') {
|
if (user?.role === 'HOD') {
|
||||||
filtered = data.filter((e: Event) => e.department === user?.department);
|
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
|
||||||
|
filtered = data.filter((e: Event) => userDepts.includes(e.department?.trim().toLowerCase()));
|
||||||
} else if (user?.role === 'FACULTY') {
|
} else if (user?.role === 'FACULTY') {
|
||||||
filtered = data.filter((e: Event) => e.proposer?.email === user?.email);
|
filtered = data.filter((e: Event) => e.proposer?.email === user?.email);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export const ApprovalsView: React.FC = () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
// Filter based on role
|
// Filter based on role
|
||||||
if (user?.role === 'HOD') {
|
if (user?.role === 'HOD') {
|
||||||
setEvents(data.filter((e: Event) => e.status === 'REQUESTED' && e.department === user.department));
|
const userDepts = user.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
|
||||||
|
setEvents(data.filter((e: Event) => e.status === 'REQUESTED' && userDepts.includes(e.department?.trim().toLowerCase())));
|
||||||
} else if (user?.role === 'PRINCIPAL') {
|
} else if (user?.role === 'PRINCIPAL') {
|
||||||
setEvents(data.filter((e: Event) => e.status === 'PENDING_PR'));
|
setEvents(data.filter((e: Event) => e.status === 'PENDING_PR'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -105,6 +105,7 @@ interface EventProposalFormProps {
|
|||||||
sponsors?: string[];
|
sponsors?: string[];
|
||||||
centreName?: string;
|
centreName?: string;
|
||||||
isPublicEvent?: boolean;
|
isPublicEvent?: boolean;
|
||||||
|
image?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
@@ -122,6 +123,42 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
|||||||
const [existingEvents, setExistingEvents] = useState<Event[]>([]);
|
const [existingEvents, setExistingEvents] = useState<Event[]>([]);
|
||||||
const [classStrengths, setClassStrengths] = useState<any[]>([]);
|
const [classStrengths, setClassStrengths] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const img = new window.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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [eventScope, setEventScope] = useState<'INSTITUTIONAL' | 'DEPARTMENT' | 'CLUB' | 'PLACEMENT' | 'CENTRE'>(
|
const [eventScope, setEventScope] = useState<'INSTITUTIONAL' | 'DEPARTMENT' | 'CLUB' | 'PLACEMENT' | 'CENTRE'>(
|
||||||
initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT')
|
initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT')
|
||||||
);
|
);
|
||||||
@@ -165,6 +202,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
|||||||
sponsors: initialData?.sponsors || [] as string[],
|
sponsors: initialData?.sponsors || [] as string[],
|
||||||
centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''),
|
centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''),
|
||||||
isPublicEvent: initialData?.isPublicEvent || false,
|
isPublicEvent: initialData?.isPublicEvent || false,
|
||||||
|
image: initialData?.image || '',
|
||||||
status: initialData?.status || 'REQUESTED'
|
status: initialData?.status || 'REQUESTED'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +460,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
|||||||
category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')),
|
category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')),
|
||||||
eventType: formData.eventType,
|
eventType: formData.eventType,
|
||||||
centreName: eventScope === 'CENTRE' ? formData.centreName : undefined,
|
centreName: eventScope === 'CENTRE' ? formData.centreName : undefined,
|
||||||
isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined
|
isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined,
|
||||||
|
image: formData.image
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(API_BASE_URL + '/api/events/propose', {
|
const res = await fetch(API_BASE_URL + '/api/events/propose', {
|
||||||
@@ -1191,6 +1230,50 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Cover Image Upload Area */}
|
||||||
|
<div className="space-y-4 pt-6">
|
||||||
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">
|
||||||
|
Event Cover Banner
|
||||||
|
</label>
|
||||||
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="w-full h-48 bg-white border-2 border-dashed border-slate-200 rounded-2xl flex flex-col items-center justify-center gap-3 hover:border-brand-indigo transition-all overflow-hidden relative group"
|
||||||
|
>
|
||||||
|
{formData.image ? (
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img src={formData.image} alt="Cover Banner" className="w-full h-full object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all flex items-center justify-center text-white text-[10px] font-black uppercase tracking-widest">
|
||||||
|
Change Cover Image
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center text-slate-400 group-hover:text-brand-indigo transition-colors">
|
||||||
|
<i className="fas fa-image text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="block text-[10px] font-black text-text-dark uppercase tracking-widest">
|
||||||
|
Upload Cover Image
|
||||||
|
</span>
|
||||||
|
<span className="block text-[8px] font-bold text-text-muted uppercase tracking-widest mt-1">
|
||||||
|
JPEG or PNG, Max width 800px (auto-resized)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleImageChange}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-6">
|
<div className="pt-6">
|
||||||
{error && <div className="mb-4 p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2"><AlertCircle className="w-4 h-4" />{error}</div>}
|
{error && <div className="mb-4 p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2"><AlertCircle className="w-4 h-4" />{error}</div>}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -261,7 +261,10 @@ export const InstitutionalChecklist: React.FC<{
|
|||||||
let depts = ALL_DEPARTMENTS;
|
let depts = ALL_DEPARTMENTS;
|
||||||
if (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') depts = ALL_DEPARTMENTS;
|
if (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') depts = ALL_DEPARTMENTS;
|
||||||
else if (isGlobalView && user?.role === 'HOD') depts = ALL_DEPARTMENTS;
|
else if (isGlobalView && user?.role === 'HOD') depts = ALL_DEPARTMENTS;
|
||||||
else depts = ALL_DEPARTMENTS.filter(d => d.toLowerCase().includes(user?.department?.toLowerCase() || ''));
|
else {
|
||||||
|
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
|
||||||
|
depts = ALL_DEPARTMENTS.filter(d => userDepts.includes(d.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
// Recent items at top logic: Sort departments by the date of their latest event
|
// Recent items at top logic: Sort departments by the date of their latest event
|
||||||
return [...depts].sort((a, b) => {
|
return [...depts].sort((a, b) => {
|
||||||
|
|||||||
@@ -178,7 +178,11 @@ export const Overview: React.FC<OverviewProps> = ({
|
|||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const statsData = (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN')
|
const statsData = (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN')
|
||||||
? data
|
? data
|
||||||
: data.filter(e => e.proposer?.email === user?.email || e.department === user?.department);
|
: data.filter(e => {
|
||||||
|
if (e.proposer?.email === user?.email) return true;
|
||||||
|
const userDepts = user?.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
|
||||||
|
return userDepts.includes(e.department?.trim().toLowerCase());
|
||||||
|
});
|
||||||
calculateStats(statsData);
|
calculateStats(statsData);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ export const StudentRegistrationsView: React.FC = () => {
|
|||||||
// Faculty can manage their proposed events, Admins/HODs can manage all
|
// Faculty can manage their proposed events, Admins/HODs can manage all
|
||||||
let filtered = data.filter((e: Event) => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing');
|
let filtered = data.filter((e: Event) => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing');
|
||||||
if (user?.role === 'FACULTY') {
|
if (user?.role === 'FACULTY') {
|
||||||
filtered = filtered.filter((e: Event) => e.department === user.department);
|
const userDepts = user.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
|
||||||
|
filtered = filtered.filter((e: Event) => userDepts.includes(e.department?.trim().toLowerCase()));
|
||||||
}
|
}
|
||||||
setEvents(filtered);
|
setEvents(filtered);
|
||||||
if (filtered.length > 0) {
|
if (filtered.length > 0) {
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ export const UserManagement: React.FC = () => {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if ((formData.role === 'HOD' || formData.role === 'FACULTY') && !formData.department.trim()) {
|
||||||
|
alert("Please select at least one department.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = editingUser
|
const url = editingUser
|
||||||
? `${API_BASE_URL}/api/admin/users/${editingUser.id}`
|
? `${API_BASE_URL}/api/admin/users/${editingUser.id}`
|
||||||
: API_BASE_URL + '/api/admin/users';
|
: API_BASE_URL + '/api/admin/users';
|
||||||
@@ -364,40 +368,70 @@ export const UserManagement: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Role</label>
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Role</label>
|
<select
|
||||||
<select
|
value={formData.role}
|
||||||
value={formData.role}
|
onChange={(e) => {
|
||||||
onChange={(e) => setFormData({...formData, role: e.target.value})}
|
const newRole = e.target.value;
|
||||||
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
|
setFormData({
|
||||||
>
|
...formData,
|
||||||
<option value="FACULTY">Faculty</option>
|
role: newRole,
|
||||||
<option value="HOD">HoD</option>
|
department: (newRole === 'HOD' || newRole === 'FACULTY') ? formData.department : ''
|
||||||
<option value="PRINCIPAL">Principal</option>
|
});
|
||||||
<option value="PLACEMENT">Placement</option>
|
}}
|
||||||
<option value="ADMIN">Admin</option>
|
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
|
||||||
</select>
|
>
|
||||||
</div>
|
<option value="FACULTY">Faculty</option>
|
||||||
<div>
|
<option value="HOD">HoD</option>
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Department</label>
|
<option value="PRINCIPAL">Principal</option>
|
||||||
<select
|
<option value="PLACEMENT">Placement</option>
|
||||||
required
|
<option value="ADMIN">Admin</option>
|
||||||
value={formData.department}
|
</select>
|
||||||
onChange={(e) => setFormData({...formData, department: e.target.value})}
|
</div>
|
||||||
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
|
|
||||||
>
|
{(formData.role === 'HOD' || formData.role === 'FACULTY') && (
|
||||||
<option value="">Select Department</option>
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Department(s)</label>
|
||||||
|
<div className="flex flex-wrap gap-2 p-4 bg-slate-50/50 rounded-2xl border border-slate-100 min-h-[60px]">
|
||||||
{[
|
{[
|
||||||
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
|
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
|
||||||
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
|
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
|
||||||
"H&S Dept", "Club", "Centre"
|
"H&S Dept", "Club", "Centre"
|
||||||
].map(dept => (
|
].map(dept => {
|
||||||
<option key={dept} value={dept}>{dept}</option>
|
const selectedDepts = formData.department ? formData.department.split(',').map(d => d.trim()).filter(Boolean) : [];
|
||||||
))}
|
const isSelected = selectedDepts.includes(dept);
|
||||||
</select>
|
return (
|
||||||
</div>
|
<button
|
||||||
</div>
|
key={dept}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
let updated: string[];
|
||||||
|
if (isSelected) {
|
||||||
|
updated = selectedDepts.filter(d => d !== dept);
|
||||||
|
} else {
|
||||||
|
updated = [...selectedDepts, dept];
|
||||||
|
}
|
||||||
|
setFormData({...formData, department: updated.join(', ')});
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all border",
|
||||||
|
isSelected
|
||||||
|
? "bg-brand-navy text-white border-brand-navy premium-shadow-sm"
|
||||||
|
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{dept}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">
|
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">
|
||||||
|
|||||||
169
RIT-EMS-main/frontend/src/docs/GOOGLE_AUTH_SETUP.md
Normal file
169
RIT-EMS-main/frontend/src/docs/GOOGLE_AUTH_SETUP.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Manual Integration Guide: Google Sign-In with Firebase Auth
|
||||||
|
|
||||||
|
This document provides step-by-step instructions on how to transition from the simulated Google login to a production-ready Google Authentication system using Firebase Auth in the React frontend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Firebase Project Setup
|
||||||
|
|
||||||
|
1. Go to the [Firebase Console](https://console.firebase.google.com/).
|
||||||
|
2. Click **Add Project** and follow the prompts to create a new project.
|
||||||
|
3. Once the project is created, click the **Web icon** (`</>`) on the project overview page to register a new web application.
|
||||||
|
4. Copy the `firebaseConfig` details provided in the Firebase Console. It will look like this:
|
||||||
|
```javascript
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: "YOUR_API_KEY",
|
||||||
|
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
|
||||||
|
projectId: "YOUR_PROJECT_ID",
|
||||||
|
storageBucket: "YOUR_PROJECT_ID.firebasestorage.app",
|
||||||
|
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
|
||||||
|
appId: "YOUR_APP_ID"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Enable Google Authentication
|
||||||
|
|
||||||
|
1. In the left-hand navigation sidebar of the Firebase Console, go to **Build** > **Authentication**.
|
||||||
|
2. Click **Get Started** if this is the first time setting up Authentication.
|
||||||
|
3. Navigate to the **Sign-in method** tab.
|
||||||
|
4. Click **Add new provider** and select **Google**.
|
||||||
|
5. Enable the toggle, configure your public-facing project name, select a project support email, and click **Save**.
|
||||||
|
6. (Optional) Under **Authorized domains**, ensure `localhost` and your production domain are listed so OAuth redirects work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Set Up Project Credentials in Google Cloud Console
|
||||||
|
|
||||||
|
Google Sign-in requires OAuth consent. If you see redirect issues:
|
||||||
|
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
||||||
|
2. Select your Firebase project from the dropdown.
|
||||||
|
3. Go to **APIs & Services** > **OAuth consent screen**.
|
||||||
|
4. Configure the publishing status to **Testing** or **Production**, add test users if in Testing, and fill out required app details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Frontend Code Integration
|
||||||
|
|
||||||
|
### 1. Install Firebase SDK
|
||||||
|
Ensure firebase dependencies are installed in your frontend folder:
|
||||||
|
```bash
|
||||||
|
npm install firebase
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Firebase Auth Client
|
||||||
|
Create/update `frontend/src/lib/firebaseConfig.ts` with the copied configurations:
|
||||||
|
```typescript
|
||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: "YOUR_API_KEY",
|
||||||
|
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
|
||||||
|
projectId: "YOUR_PROJECT_ID",
|
||||||
|
storageBucket: "YOUR_PROJECT_ID.firebasestorage.app",
|
||||||
|
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
|
||||||
|
appId: "YOUR_APP_ID"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Firebase
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
|
||||||
|
// Initialize Firebase Auth
|
||||||
|
export const auth = getAuth(app);
|
||||||
|
export const googleProvider = new GoogleAuthProvider();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Implement Sign-in Action
|
||||||
|
In `LoginPage.tsx`, replace the mock handlers with real Firebase Auth sign-in triggers:
|
||||||
|
```typescript
|
||||||
|
import { signInWithPopup } from 'firebase/auth';
|
||||||
|
import { auth, googleProvider } from '../lib/firebaseConfig';
|
||||||
|
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
try {
|
||||||
|
const result = await signInWithPopup(auth, googleProvider);
|
||||||
|
const user = result.user;
|
||||||
|
const email = user.email; // e.g. student.240007@cse.ritchennai.edu.in
|
||||||
|
const displayName = user.displayName || "RIT User";
|
||||||
|
|
||||||
|
// Call your backend/database lookup or sync user details
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, fullName: displayName })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
login(userData);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
alert(errorData.message || "Failed to log in.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Google Sign-In Error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Backend Integration (Spring Boot)
|
||||||
|
|
||||||
|
To support Google authentication token validation or syncing, add a secure endpoint:
|
||||||
|
```java
|
||||||
|
@PostMapping("/api/auth/google-login")
|
||||||
|
public ResponseEntity<?> googleLogin(@RequestBody Map<String, String> payload) {
|
||||||
|
String email = payload.get("email");
|
||||||
|
String fullName = payload.get("fullName");
|
||||||
|
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Email is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
Optional<User> existingUser = userRepository.findByEmail(email);
|
||||||
|
if (existingUser.isPresent()) {
|
||||||
|
return ResponseEntity.ok(existingUser.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register RIT student if email matches the student format
|
||||||
|
if (email.matches("^student\\.\\d{6}@[a-zA-Z0-9&-_]+\\.ritchennai\\.edu\\.in$")) {
|
||||||
|
User newStudent = new User();
|
||||||
|
newStudent.setEmail(email);
|
||||||
|
newStudent.setFullName(fullName);
|
||||||
|
newStudent.setRole("STUDENT");
|
||||||
|
|
||||||
|
// Parse department & batch logic here
|
||||||
|
String rollNumber = email.split("@")[0].substring(8); // extracts roll e.g. "240007"
|
||||||
|
String rawDept = email.split("@")[1].split("\\.")[0]; // cse
|
||||||
|
|
||||||
|
newStudent.setRegNo(rollNumber);
|
||||||
|
|
||||||
|
// Set Department
|
||||||
|
newStudent.setDepartment(mapDepartmentDomain(rawDept));
|
||||||
|
|
||||||
|
// Set Year Batch
|
||||||
|
int joinYear = 2000 + Integer.parseInt(rollNumber.substring(0, 2));
|
||||||
|
int currentYear = LocalDate.now().getYear();
|
||||||
|
int academicOffset = LocalDate.now().getMonthValue() >= 6 ? 1 : 0;
|
||||||
|
int yearIndex = currentYear - joinYear + academicOffset;
|
||||||
|
String[] years = {"1st Year", "2nd Year", "3rd Year", "4th Year"};
|
||||||
|
newStudent.setInchargeBatch(yearIndex >= 1 && yearIndex <= 4 ? years[yearIndex - 1] : "N/A");
|
||||||
|
newStudent.setInchargeClass(newStudent.getDepartment());
|
||||||
|
newStudent.setInchargeSection("A");
|
||||||
|
|
||||||
|
// Generate random placeholder password for DB constraints
|
||||||
|
newStudent.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||||
|
|
||||||
|
userRepository.save(newStudent);
|
||||||
|
return ResponseEntity.ok(newStudent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(401).body(Map.of("message", "Access denied. Only registered accounts can log in."));
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
updateDoc,
|
updateDoc,
|
||||||
deleteDoc
|
deleteDoc
|
||||||
} from 'firebase/firestore';
|
} from 'firebase/firestore';
|
||||||
|
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyBdRUyA7LDtDReUA3TXDys71dSHgD2tOEA",
|
apiKey: "AIzaSyBdRUyA7LDtDReUA3TXDys71dSHgD2tOEA",
|
||||||
@@ -21,8 +22,10 @@ const firebaseConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initialize Firebase App
|
// Initialize Firebase App
|
||||||
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
|
export const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
|
||||||
const db = getFirestore(app);
|
export const db = getFirestore(app);
|
||||||
|
export const auth = getAuth(app);
|
||||||
|
export const googleProvider = new GoogleAuthProvider();
|
||||||
|
|
||||||
// Helper: parse date strings into Date objects
|
// Helper: parse date strings into Date objects
|
||||||
function parseDate(val: any): Date {
|
function parseDate(val: any): Date {
|
||||||
@@ -218,8 +221,9 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
|||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
if (path === '/api/auth/login' && method === 'POST') {
|
if (path === '/api/auth/login' && method === 'POST') {
|
||||||
const { email, password } = parseBody(init?.body);
|
const { email, password } = parseBody(init?.body);
|
||||||
|
const emailLower = (email || '').trim().toLowerCase();
|
||||||
const snap = await getDocs(collection(db, 'ems_users'));
|
const snap = await getDocs(collection(db, 'ems_users'));
|
||||||
const userDoc = snap.docs.find(d => d.data().email?.toLowerCase() === email?.toLowerCase());
|
const userDoc = snap.docs.find(d => d.data().email?.toLowerCase() === emailLower);
|
||||||
|
|
||||||
if (userDoc) {
|
if (userDoc) {
|
||||||
const u = userDoc.data();
|
const u = userDoc.data();
|
||||||
@@ -231,9 +235,166 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
|||||||
department: userData.department || "N/A",
|
department: userData.department || "N/A",
|
||||||
assignedClubs: userData.assignedClubs || []
|
assignedClubs: userData.assignedClubs || []
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return jsonResponse({ message: "Invalid password for this registered email" }, 401);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return jsonResponse({ message: "Invalid email or passcode" }, 401);
|
|
||||||
|
// Auto-register student if matches roll number format
|
||||||
|
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 newUser = {
|
||||||
|
id: generateNumericId(),
|
||||||
|
email: emailLower,
|
||||||
|
password: password || "student123",
|
||||||
|
fullName: `Student ${rollNo}`,
|
||||||
|
role: 'STUDENT',
|
||||||
|
department: dept,
|
||||||
|
year: calculatedYear,
|
||||||
|
section: 'A',
|
||||||
|
regNo: rollNo,
|
||||||
|
phone: '',
|
||||||
|
gender: 'Male',
|
||||||
|
collegeName: 'Rajalakshmi Institute of Technology',
|
||||||
|
isClubCoordinator: false,
|
||||||
|
isPlacementStaff: false,
|
||||||
|
isClassIncharge: false,
|
||||||
|
assignedClubs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
await setDoc(doc(db, 'ems_users', String(newUser.id)), newUser);
|
||||||
|
|
||||||
|
const { password: _, ...userData } = newUser;
|
||||||
|
return jsonResponse({
|
||||||
|
...userData,
|
||||||
|
department: userData.department || "N/A",
|
||||||
|
assignedClubs: userData.assignedClubs || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse({ message: "Invalid email or passcode. Faculty and Admin roles must be pre-registered by the administrator." }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/api/auth/google-login' && method === 'POST') {
|
||||||
|
const { email, fullName } = parseBody(init?.body);
|
||||||
|
const emailLower = (email || '').trim().toLowerCase();
|
||||||
|
const snap = await getDocs(collection(db, 'ems_users'));
|
||||||
|
const userDoc = snap.docs.find(d => d.data().email?.toLowerCase() === emailLower);
|
||||||
|
|
||||||
|
if (userDoc) {
|
||||||
|
const u = userDoc.data();
|
||||||
|
const { password: _, ...userData } = u;
|
||||||
|
return jsonResponse({
|
||||||
|
...userData,
|
||||||
|
department: userData.department || "N/A",
|
||||||
|
assignedClubs: userData.assignedClubs || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if matches student RIT format
|
||||||
|
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 newUser = {
|
||||||
|
id: generateNumericId(),
|
||||||
|
email: emailLower,
|
||||||
|
password: 'google-oauth-placeholder',
|
||||||
|
fullName: fullName || `Student ${rollNo}`,
|
||||||
|
role: 'STUDENT',
|
||||||
|
department: dept,
|
||||||
|
year: calculatedYear,
|
||||||
|
section: 'A',
|
||||||
|
regNo: rollNo,
|
||||||
|
phone: '',
|
||||||
|
gender: 'Male',
|
||||||
|
collegeName: 'Rajalakshmi Institute of Technology',
|
||||||
|
isClubCoordinator: false,
|
||||||
|
isPlacementStaff: false,
|
||||||
|
isClassIncharge: false,
|
||||||
|
assignedClubs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
await setDoc(doc(db, 'ems_users', String(newUser.id)), newUser);
|
||||||
|
|
||||||
|
const { password: _, ...userData } = newUser;
|
||||||
|
return jsonResponse({
|
||||||
|
...userData,
|
||||||
|
department: userData.department || "N/A",
|
||||||
|
assignedClubs: userData.assignedClubs || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register external students automatically
|
||||||
|
if (emailLower) {
|
||||||
|
const newUser = {
|
||||||
|
id: generateNumericId(),
|
||||||
|
email: emailLower,
|
||||||
|
password: 'google-oauth-placeholder',
|
||||||
|
fullName: fullName || emailLower.split('@')[0],
|
||||||
|
role: 'STUDENT',
|
||||||
|
department: 'Others',
|
||||||
|
year: 'N/A',
|
||||||
|
section: 'N/A',
|
||||||
|
regNo: 'EXT-' + Math.floor(Math.random() * 100000),
|
||||||
|
phone: '',
|
||||||
|
gender: 'Male',
|
||||||
|
collegeName: 'External Institution',
|
||||||
|
isClubCoordinator: false,
|
||||||
|
isPlacementStaff: false,
|
||||||
|
isClassIncharge: false,
|
||||||
|
assignedClubs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
await setDoc(doc(db, 'ems_users', String(newUser.id)), newUser);
|
||||||
|
|
||||||
|
const { password: _, ...userData } = newUser;
|
||||||
|
return jsonResponse({
|
||||||
|
...userData,
|
||||||
|
department: userData.department || "N/A",
|
||||||
|
assignedClubs: userData.assignedClubs || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse({ message: "Access denied. Only registered accounts can log in." }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
@@ -252,13 +413,16 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
|||||||
return jsonResponse({ message: "User with this email already exists" }, 400);
|
return jsonResponse({ message: "User with this email already exists" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const role = payload.role || 'FACULTY';
|
||||||
|
const department = (role === 'HOD' || role === 'FACULTY') ? (payload.department || 'H&S Dept') : '';
|
||||||
|
|
||||||
const newUser = {
|
const newUser = {
|
||||||
...payload,
|
...payload,
|
||||||
id: generateNumericId(),
|
id: generateNumericId(),
|
||||||
email: payload.email?.trim().toLowerCase(),
|
email: payload.email?.trim().toLowerCase(),
|
||||||
fullName: payload.fullName?.trim(),
|
fullName: payload.fullName?.trim(),
|
||||||
role: payload.role || 'FACULTY',
|
role: role,
|
||||||
department: payload.department || 'H&S Dept',
|
department: department,
|
||||||
assignedClubs: payload.assignedClubs || []
|
assignedClubs: payload.assignedClubs || []
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -277,9 +441,15 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
|||||||
return jsonResponse({ message: "User not found" }, 404);
|
return jsonResponse({ message: "User not found" }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const role = payload.role || snap.data().role || 'FACULTY';
|
||||||
|
const department = (role === 'HOD' || role === 'FACULTY')
|
||||||
|
? (payload.department !== undefined ? payload.department : snap.data().department || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
const updated = {
|
const updated = {
|
||||||
...snap.data(),
|
...snap.data(),
|
||||||
...payload,
|
...payload,
|
||||||
|
department: department,
|
||||||
id: snap.data().id // lock ID
|
id: snap.data().id // lock ID
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import ritLogo from '../assets/images/college-logo.png';
|
import ritLogo from '../assets/images/college-logo.png';
|
||||||
|
import { Mail, Lock, LogIn, AlertCircle, X, ArrowLeft, Shield } from 'lucide-react';
|
||||||
type UserRole = 'STUDENT' | 'COORDINATOR' | 'ADMIN';
|
import { signInWithPopup } from 'firebase/auth';
|
||||||
|
import { auth, googleProvider } from '../lib/firebaseBackend';
|
||||||
|
|
||||||
const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S'];
|
const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S'];
|
||||||
const EXTERNAL_DEPARTMENTS = [
|
const EXTERNAL_DEPARTMENTS = [
|
||||||
@@ -22,14 +23,48 @@ const EXTERNAL_DEPARTMENTS = [
|
|||||||
const SECTIONS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
const SECTIONS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
||||||
const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year'];
|
const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year'];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
export const LoginPage: React.FC = () => {
|
export const LoginPage: React.FC = () => {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [selectedRole, setSelectedRole] = useState<UserRole | null>(null);
|
|
||||||
|
|
||||||
// Forms states
|
|
||||||
const [isSignUp, setIsSignUp] = useState(false);
|
const [isSignUp, setIsSignUp] = useState(false);
|
||||||
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
|
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
|
||||||
const [coordinatorLoginType, setCoordinatorLoginType] = useState<'Faculty' | 'HOD' | null>(null);
|
const [splashState, setSplashState] = useState<'logo' | 'text' | 'fade-to-white' | 'done'>('logo');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer1 = setTimeout(() => {
|
||||||
|
setSplashState('text');
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
const timer2 = setTimeout(() => {
|
||||||
|
setSplashState('fade-to-white');
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const timer3 = setTimeout(() => {
|
||||||
|
setSplashState('done');
|
||||||
|
}, 4500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer1);
|
||||||
|
clearTimeout(timer2);
|
||||||
|
clearTimeout(timer3);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Login credentials state
|
||||||
|
const [emailInput, setEmailInput] = useState('');
|
||||||
|
const [passwordInput, setPasswordInput] = useState('');
|
||||||
|
|
||||||
|
// Register state (for other colleges / external students)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
@@ -49,99 +84,157 @@ export const LoginPage: React.FC = () => {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
// Auto-set internal for non-student roles
|
// Google sign in state
|
||||||
useEffect(() => {
|
const [showGoogleChooser, setShowGoogleChooser] = useState(false);
|
||||||
if (selectedRole && selectedRole !== 'STUDENT') {
|
const [customGoogleEmail, setCustomGoogleEmail] = useState('');
|
||||||
setSignUpType('INTERNAL');
|
|
||||||
} else if (!isSignUp) {
|
const mockAccounts = [
|
||||||
setSignUpType(null);
|
{ email: "student.240007@cse.ritchennai.edu.in", name: "Abiram R (3rd Year CSE)", role: "Student (Auto-Reg)" },
|
||||||
}
|
{ email: "student.250012@aiml.ritchennai.edu.in", name: "John Doe (2nd Year AIML)", role: "Student (Auto-Reg)" },
|
||||||
}, [selectedRole, isSignUp]);
|
{ email: "faculty@rit.edu", name: "Dr. Faculty Member", role: "Faculty" },
|
||||||
|
{ email: "hod@rit.edu", name: "Prof. Head of Dept", role: "HOD" },
|
||||||
|
{ email: "principal@rit.edu", name: "Dr. College Principal", role: "Principal" },
|
||||||
|
{ email: "admin@rit.edu", name: "System Administrator", role: "Admin" },
|
||||||
|
];
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleLoginSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isSignUp) {
|
const response = await fetch(API_BASE_URL + '/api/auth/login', {
|
||||||
// Login Logic
|
method: 'POST',
|
||||||
const response = await fetch(API_BASE_URL + '/api/auth/login', {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: emailInput,
|
||||||
|
password: passwordInput
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
login(userData);
|
||||||
|
} else {
|
||||||
|
const errData = await response.json();
|
||||||
|
throw new Error(errData.message || "Invalid credentials.");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMessage(err.message || "An error occurred during authentication.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await signInWithPopup(auth, googleProvider);
|
||||||
|
const user = result.user;
|
||||||
|
if (user && user.email) {
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: formData.email,
|
email: user.email,
|
||||||
password: formData.password
|
fullName: user.displayName || "RIT User"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
|
|
||||||
// Role checks
|
|
||||||
if (selectedRole === 'STUDENT' && userData.role !== 'STUDENT') {
|
|
||||||
throw new Error("Access Denied: Please use the Staff Portal to log in.");
|
|
||||||
}
|
|
||||||
if (selectedRole === 'COORDINATOR') {
|
|
||||||
const isCoordRole = userData.role === 'FACULTY' || userData.role === 'HOD';
|
|
||||||
if (!isCoordRole) {
|
|
||||||
throw new Error("Access Denied: You are not authorized for Coordinator Portal.");
|
|
||||||
}
|
|
||||||
if (coordinatorLoginType === 'HOD' && userData.role !== 'HOD') {
|
|
||||||
throw new Error("Access Denied: HOD access only.");
|
|
||||||
}
|
|
||||||
if (coordinatorLoginType === 'Faculty' && userData.role !== 'FACULTY') {
|
|
||||||
throw new Error("Access Denied: Faculty access only.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selectedRole === 'ADMIN' && userData.role !== 'ADMIN' && userData.role !== 'PRINCIPAL') {
|
|
||||||
throw new Error("Access Denied: Admin or Principal access only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
login(userData);
|
login(userData);
|
||||||
} else {
|
} else {
|
||||||
const errData = await response.json();
|
const errData = await response.json();
|
||||||
throw new Error(errData.message || "Invalid credentials.");
|
throw new Error(errData.message || "Failed to sync Google account with database.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} 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 handleGoogleLoginSubmit = async (email: string, name: string) => {
|
||||||
|
if (!email) {
|
||||||
|
alert("Please select or enter a Google account email.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setShowGoogleChooser(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/auth/google-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
fullName: name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
login(userData);
|
||||||
} else {
|
} else {
|
||||||
// Sign-Up Logic (Only for Student Role)
|
const errData = await response.json();
|
||||||
if (formData.password !== formData.confirmPassword) {
|
throw new Error(errData.message || "OAuth login failed.");
|
||||||
throw new Error("Passwords do not match.");
|
}
|
||||||
}
|
} catch (err: any) {
|
||||||
|
setErrorMessage(err.message || "Google Sign-In failed.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const signupPayload = {
|
const handleRegisterSubmit = async (e: React.FormEvent) => {
|
||||||
email: formData.email,
|
e.preventDefault();
|
||||||
password: formData.password,
|
setErrorMessage(null);
|
||||||
fullName: formData.name,
|
setIsSubmitting(true);
|
||||||
regNo: formData.regNo,
|
|
||||||
phone: formData.phone,
|
|
||||||
gender: formData.gender,
|
|
||||||
collegeName: signUpType === 'EXTERNAL' ? formData.collegeName : 'Rajalakshmi Institute of Technology',
|
|
||||||
department: signUpType === 'EXTERNAL' ? formData.department : formData.department,
|
|
||||||
year: signUpType === 'EXTERNAL' ? 'N/A' : formData.year,
|
|
||||||
section: signUpType === 'EXTERNAL' ? 'N/A' : formData.section,
|
|
||||||
role: 'STUDENT'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(API_BASE_URL + '/api/auth/signup', {
|
try {
|
||||||
method: 'POST',
|
if (formData.password !== formData.confirmPassword) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
throw new Error("Passwords do not match.");
|
||||||
body: JSON.stringify(signupPayload)
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
const signupPayload = {
|
||||||
const userData = await response.json();
|
email: formData.email,
|
||||||
alert("Account registered successfully!");
|
password: formData.password,
|
||||||
login(userData);
|
fullName: formData.name,
|
||||||
} else {
|
regNo: formData.regNo,
|
||||||
const errData = await response.json();
|
phone: formData.phone,
|
||||||
throw new Error(errData.message || "Registration failed.");
|
gender: formData.gender,
|
||||||
}
|
collegeName: signUpType === 'EXTERNAL' ? formData.collegeName : 'Rajalakshmi Institute of Technology',
|
||||||
|
department: formData.department,
|
||||||
|
year: signUpType === 'EXTERNAL' ? 'N/A' : formData.year,
|
||||||
|
section: signUpType === 'EXTERNAL' ? 'N/A' : formData.section,
|
||||||
|
role: 'STUDENT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/auth/signup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(signupPayload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
alert("Account registered successfully!");
|
||||||
|
login(userData);
|
||||||
|
} else {
|
||||||
|
const errData = await response.json();
|
||||||
|
throw new Error(errData.message || "Registration failed.");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setErrorMessage(err.message || "An error occurred.");
|
setErrorMessage(err.message || "An error occurred.");
|
||||||
@@ -154,350 +247,342 @@ export const LoginPage: React.FC = () => {
|
|||||||
const RIT_BLUE_TEXT = 'text-[#004a99]';
|
const RIT_BLUE_TEXT = 'text-[#004a99]';
|
||||||
const RIT_BLUE_HOVER = 'hover:bg-[#003366]';
|
const RIT_BLUE_HOVER = 'hover:bg-[#003366]';
|
||||||
|
|
||||||
const portals = [
|
|
||||||
{
|
|
||||||
id: 'STUDENT' as UserRole,
|
|
||||||
title: 'Student Portal',
|
|
||||||
description: 'Access events, track registrations, and manage your academic profile.',
|
|
||||||
icon: 'fa-user-graduate',
|
|
||||||
color: 'bg-[#004a99]',
|
|
||||||
hoverColor: 'hover:border-[#004a99]',
|
|
||||||
textColor: 'text-[#004a99]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'COORDINATOR' as UserRole,
|
|
||||||
title: 'Event Coordinator Portal',
|
|
||||||
description: 'Create, manage, and coordinate campus events and announcements.',
|
|
||||||
icon: 'fa-calendar-check',
|
|
||||||
color: 'bg-orange-500',
|
|
||||||
hoverColor: 'hover:border-orange-500',
|
|
||||||
textColor: 'text-orange-500'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ADMIN' as UserRole,
|
|
||||||
title: 'Admin Portal',
|
|
||||||
description: 'System-wide oversight, user management, and high-level analytics.',
|
|
||||||
icon: 'fa-user-shield',
|
|
||||||
color: 'bg-[#004a99]',
|
|
||||||
hoverColor: 'hover:border-[#004a99]',
|
|
||||||
textColor: 'text-[#004a99]'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-white flex items-center justify-center font-sans overflow-x-hidden">
|
<div className="min-h-screen w-full bg-slate-50 flex items-center justify-center font-sans p-6 overflow-x-hidden relative">
|
||||||
|
|
||||||
|
{/* Background radial accent */}
|
||||||
|
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-glow rounded-full blur-[120px] opacity-40 pointer-events-none -z-10" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-indigo-50 rounded-full blur-[120px] opacity-40 pointer-events-none -z-10" />
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{!selectedRole ? (
|
{!isSignUp ? (
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// WELCOME SCREEN GATEWAY
|
// CENTRALIZED SIGN IN
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
<motion.div
|
<motion.div
|
||||||
key="welcome"
|
key="login-pane"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||||
className="w-full max-w-6xl px-6 py-12 text-center"
|
transition={{ duration: 0.4 }}
|
||||||
|
className="w-full max-w-md bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-10 flex flex-col items-center relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="mb-16">
|
<div className="w-full text-center mb-8">
|
||||||
<img src={ritLogo} alt="RIT Logo" className="h-16 md:h-20 w-auto mx-auto mb-8 object-contain" />
|
<img src={ritLogo} alt="RIT Logo" className="h-16 w-auto mx-auto mb-6 object-contain" />
|
||||||
<p className="text-slate-500 text-lg font-medium max-w-2xl mx-auto uppercase tracking-widest text-xs">
|
<h2 className="text-3xl font-black text-slate-900 tracking-tight flex items-center justify-center gap-1.5 uppercase">
|
||||||
Select your gateway to excellence. Connect, manage, and celebrate.
|
RIT EVENT HUB
|
||||||
</p>
|
</h2>
|
||||||
|
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-1">Centralized Portal Login</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-8 text-left">
|
{/* Google Sign In Button */}
|
||||||
{portals.map((portal) => (
|
<button
|
||||||
<div
|
onClick={handleGoogleSignIn}
|
||||||
key={portal.id}
|
onDoubleClick={() => setShowGoogleChooser(true)}
|
||||||
onClick={() => {
|
title="Double-click to open mock accounts for local testing"
|
||||||
setSelectedRole(portal.id);
|
className="w-full py-4 px-6 bg-white border border-slate-200 hover:border-brand-indigo/30 rounded-2xl flex items-center justify-center text-xs font-black text-slate-700 uppercase tracking-widest hover:bg-slate-50 transition-all hover:scale-[1.02] active:scale-[0.98] premium-shadow-sm mb-6"
|
||||||
setErrorMessage(null);
|
>
|
||||||
setIsSignUp(false);
|
<GoogleIcon />
|
||||||
setSignUpType(null);
|
Sign in with Google
|
||||||
setCoordinatorLoginType(null);
|
</button>
|
||||||
}}
|
|
||||||
className={cn(
|
{/* Separator */}
|
||||||
"group relative bg-white border-2 border-slate-100 rounded-[3rem] p-10 cursor-pointer transition-all duration-500 hover:-translate-y-4 hover:shadow-[0_40px_80px_-20px_rgba(0,0,0,0.1)]",
|
<div className="w-full flex items-center justify-center gap-4 mb-6">
|
||||||
portal.hoverColor
|
<div className="h-px bg-slate-100 flex-1" />
|
||||||
)}
|
<span className="text-[9px] font-black text-slate-300 uppercase tracking-widest">or use credentials</span>
|
||||||
|
<div className="h-px bg-slate-100 flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email & Password Form */}
|
||||||
|
<form onSubmit={handleLoginSubmit} className="w-full space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
placeholder="EMAIL ADDRESS"
|
||||||
|
value={emailInput}
|
||||||
|
onChange={(e) => setEmailInput(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-transparent focus:border-brand-indigo/30 rounded-2xl pl-12 pr-6 py-4.5 text-xs font-bold outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
placeholder="PASSWORD"
|
||||||
|
value={passwordInput}
|
||||||
|
onChange={(e) => setPasswordInput(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-transparent focus:border-brand-indigo/30 rounded-2xl pl-12 pr-12 py-4.5 text-xs font-bold outline-none transition-all"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-5 top-1/2 -translate-y-1/2 text-slate-300 hover:text-brand-indigo transition-colors"
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<i className={cn("fas", showPassword ? "fa-eye-slash" : "fa-eye")}></i>
|
||||||
"w-20 h-20 rounded-3xl flex items-center justify-center mb-8 shadow-lg group-hover:scale-110 transition-transform duration-500",
|
</button>
|
||||||
portal.color
|
</div>
|
||||||
)}>
|
|
||||||
<i className={cn("fas text-3xl text-white", portal.icon)}></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight mb-4 group-hover:text-orange-500 transition-colors">
|
{errorMessage && (
|
||||||
{portal.title}
|
<div className="p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2 border border-red-100/50">
|
||||||
</h3>
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span>{errorMessage}</span>
|
||||||
<p className="text-slate-500 text-sm font-medium leading-relaxed mb-8">
|
|
||||||
{portal.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={cn("flex items-center gap-3 text-xs font-black uppercase tracking-widest group-hover:gap-5 transition-all", portal.textColor)}>
|
|
||||||
Enter Portal <i className="fas fa-arrow-right"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-20">
|
<button
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.5em]">
|
type="submit"
|
||||||
© 2026 Rajalakshmi Institute of Technology • Academic Excellence
|
disabled={isSubmitting}
|
||||||
</p>
|
className={cn(
|
||||||
|
"w-full py-5 text-white text-xs font-black rounded-2xl transition-all shadow-lg active:scale-[0.98] disabled:opacity-55 flex items-center justify-center gap-2 mt-4",
|
||||||
|
RIT_BLUE,
|
||||||
|
RIT_BLUE_HOVER
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
{isSubmitting ? 'Verifying...' : 'Sign in now...'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center space-y-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsSignUp(true);
|
||||||
|
setSignUpType('EXTERNAL');
|
||||||
|
setErrorMessage(null);
|
||||||
|
}}
|
||||||
|
className="text-xs font-bold text-slate-400 hover:text-brand-indigo transition-colors block mx-auto"
|
||||||
|
>
|
||||||
|
External Student ? Register here..
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// DUAL SLIDING LOGIN & SIGNUP FORMS (Site 2 Layout)
|
// SIGN UP (Only for External Students)
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
<motion.div
|
<motion.div
|
||||||
key="forms"
|
key="signup-pane"
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||||
className="relative w-full max-w-5xl h-[700px] bg-white rounded-[3rem] shadow-[0_50px_100px_-20px_rgba(0,0,0,0.15)] overflow-hidden flex flex-col md:flex-row border border-slate-100"
|
transition={{ duration: 0.4 }}
|
||||||
|
className="w-full max-w-md bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-10 flex flex-col relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedRole(null)}
|
onClick={() => {
|
||||||
className="absolute top-8 left-8 z-[60] w-12 h-12 bg-white/10 md:bg-slate-100 hover:bg-slate-200 text-slate-500 rounded-full flex items-center justify-center transition-all border border-transparent shadow-sm"
|
setIsSignUp(false);
|
||||||
|
setSignUpType(null);
|
||||||
|
setErrorMessage(null);
|
||||||
|
}}
|
||||||
|
className="absolute top-6 left-6 w-10 h-10 bg-slate-50 hover:bg-slate-100 text-slate-500 rounded-full flex items-center justify-center transition-all border border-slate-100"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="fas fa-arrow-left"></i>
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Forms Section */}
|
<div className="text-center mb-8 pt-4">
|
||||||
<div className="relative flex-1 flex">
|
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
|
||||||
|
Other College <span className="text-[#004a99]">Sign Up</span>
|
||||||
{/* Sign Up (Left Side Panel) */}
|
</h2>
|
||||||
<div className={cn(
|
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
||||||
"absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10",
|
|
||||||
isSignUp ? "opacity-100 translate-x-0 visible" : "opacity-0 -translate-x-full invisible pointer-events-none"
|
|
||||||
)}>
|
|
||||||
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain self-start" />
|
|
||||||
|
|
||||||
{!signUpType && selectedRole === '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>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setSignUpType('INTERNAL')}
|
|
||||||
type="button"
|
|
||||||
className={cn("w-full py-6 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95", RIT_BLUE)}
|
|
||||||
>
|
|
||||||
RIT Student (Internal)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setSignUpType('EXTERNAL')}
|
|
||||||
type="button"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Other College (External)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 overflow-y-auto no-scrollbar py-4 pr-1">
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
|
|
||||||
{signUpType === 'INTERNAL' ? 'Internal' : 'External'} <span className="text-orange-500">Sign Up</span>
|
|
||||||
</h2>
|
|
||||||
<button type="button" onClick={() => setSignUpType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<input required name="name" placeholder="FULL NAME" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.name} onChange={handleInputChange} />
|
|
||||||
<input required type="email" name="email" placeholder="EMAIL" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<input required name="regNo" placeholder="REG NO" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.regNo} onChange={handleInputChange} />
|
|
||||||
<input required name="phone" placeholder="PHONE NUMBER" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.phone} onChange={handleInputChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<input required type="password" name="password" placeholder="PASSWORD" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
|
|
||||||
<input required type="password" name="confirmPassword" placeholder="CONFIRM" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.confirmPassword} onChange={handleInputChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<select required name="gender" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all cursor-pointer" value={formData.gender} onChange={handleInputChange}>
|
|
||||||
<option value="">SELECT GENDER</option>
|
|
||||||
<option value="Male">MALE</option>
|
|
||||||
<option value="Female">FEMALE</option>
|
|
||||||
<option value="Other">OTHER</option>
|
|
||||||
</select>
|
|
||||||
<input required name="collegeLocation" placeholder="COLLEGE LOCATION" className="bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.collegeLocation} onChange={handleInputChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
name="collegeName"
|
|
||||||
placeholder="COLLEGE NAME"
|
|
||||||
disabled={signUpType === 'INTERNAL'}
|
|
||||||
className="w-full bg-slate-50 border-none rounded-2xl px-6 py-4 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all disabled:opacity-60"
|
|
||||||
value={formData.collegeName}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<select required name="department" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.department} onChange={handleInputChange}>
|
|
||||||
<option value="">DEPT</option>
|
|
||||||
{(signUpType === 'EXTERNAL' ? EXTERNAL_DEPARTMENTS : DEPARTMENTS).map(d => <option key={d} value={d}>{d}</option>)}
|
|
||||||
</select>
|
|
||||||
<select required name="year" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.year} onChange={handleInputChange}>
|
|
||||||
<option value="">YEAR</option>
|
|
||||||
{YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
|
||||||
</select>
|
|
||||||
<select required name="section" className="bg-slate-50 border-none rounded-2xl px-4 py-4 text-[10px] font-black outline-none cursor-pointer" value={formData.section} onChange={handleInputChange}>
|
|
||||||
<option value="">SEC</option>
|
|
||||||
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{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 disabled:opacity-55">
|
|
||||||
{isSubmitting ? 'Registering...' : 'Sign Up Now'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sign In (Right Side Panel) */}
|
|
||||||
<div className={cn(
|
|
||||||
"absolute inset-0 w-full md:w-1/2 h-full flex flex-col justify-center p-12 transition-all duration-700 ease-in-out z-10 ml-auto",
|
|
||||||
!isSignUp ? "opacity-100 translate-x-0 visible" : "opacity-0 translate-x-full invisible pointer-events-none"
|
|
||||||
)}>
|
|
||||||
|
|
||||||
{selectedRole === 'COORDINATOR' && !coordinatorLoginType ? (
|
|
||||||
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
|
|
||||||
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain mx-auto" />
|
|
||||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-8">Choose <span className="text-orange-500">Access Level</span></h2>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setCoordinatorLoginType('Faculty')}
|
|
||||||
type="button"
|
|
||||||
className={cn("w-full py-6 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95", RIT_BLUE)}
|
|
||||||
>
|
|
||||||
Faculty Member
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCoordinatorLoginType('HOD')}
|
|
||||||
type="button"
|
|
||||||
className="w-full py-6 bg-orange-500 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg transition-all hover:scale-[1.02] active:scale-95"
|
|
||||||
>
|
|
||||||
Head of Department
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="mb-8">
|
|
||||||
<img src={ritLogo} alt="Logo" className="h-12 w-auto mb-6 object-contain" />
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-4xl font-black text-slate-900 uppercase tracking-tighter">Welcome <span className={RIT_BLUE_TEXT}>Back</span></h2>
|
|
||||||
{selectedRole === 'COORDINATOR' && (
|
|
||||||
<button type="button" onClick={() => setCoordinatorLoginType(null)} className="text-[10px] font-black text-slate-400 uppercase hover:text-orange-500">Change</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-2">
|
|
||||||
Access your {selectedRole === 'ADMIN' ? 'Admin / Principal' : (selectedRole === 'COORDINATOR' ? `${coordinatorLoginType} Event` : 'Student')} portal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="relative">
|
|
||||||
<i className="fas fa-envelope absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
|
|
||||||
<input required type="email" name="email" placeholder="EMAIL ADDRESS" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-6 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.email} onChange={handleInputChange} />
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<i className="fas fa-lock absolute left-6 top-1/2 -translate-y-1/2 text-slate-300"></i>
|
|
||||||
<input required type={showPassword ? 'text' : 'password'} name="password" placeholder="PASSWORD" className="w-full bg-slate-50 border-none rounded-2xl pl-14 pr-14 py-5 text-xs font-bold outline-none focus:ring-2 focus:ring-[#004a99] transition-all" value={formData.password} onChange={handleInputChange} />
|
|
||||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className={cn("absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 transition-colors", `hover:${RIT_BLUE_TEXT}`)}>
|
|
||||||
<i className={cn("fas", showPassword ? "fa-eye-slash" : "fa-eye")}></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button type="button" className="text-[10px] font-black text-slate-400 uppercase tracking-widest hover:text-orange-500 transition-colors">Forgot Password?</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{errorMessage && <p className="text-rose-500 text-[10px] font-bold uppercase">{errorMessage}</p>}
|
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting} className={cn("w-full py-5 text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-2xl transition-all shadow-lg active:scale-95 disabled:opacity-55", RIT_BLUE, RIT_BLUE_HOVER)}>
|
|
||||||
{isSubmitting ? 'Verifying...' : 'Sign In Now'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sliding Overlay Panel (Matches RIT Navy theme) */}
|
<form onSubmit={handleRegisterSubmit} className="space-y-4 overflow-y-auto max-h-[450px] pr-3 py-1 scrollbar-hide">
|
||||||
<div className={cn(
|
<div className="grid grid-cols-2 gap-4">
|
||||||
"absolute top-0 left-0 w-full md:w-1/2 h-full z-50 transition-all duration-700 ease-[cubic-bezier(0.7,0,0.3,1)] flex flex-col items-center justify-center text-center p-12 overflow-hidden",
|
<input required name="name" placeholder="FULL NAME" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.name} onChange={handleInputChange} />
|
||||||
RIT_BLUE,
|
<input required type="email" name="email" placeholder="EMAIL" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.email} onChange={handleInputChange} />
|
||||||
isSignUp ? "md:translate-x-full" : "translate-x-0"
|
|
||||||
)}>
|
|
||||||
{/* Background patterns */}
|
|
||||||
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_white_1px,_transparent_1px)] bg-[size:30px_30px]"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Overlay Message */}
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className={cn(
|
<input required name="regNo" placeholder="REG NO" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.regNo} onChange={handleInputChange} />
|
||||||
"absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100",
|
<input required name="phone" placeholder="PHONE NUMBER" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.phone} onChange={handleInputChange} />
|
||||||
isSignUp ? "opacity-0 -translate-y-10 pointer-events-none invisible" : "opacity-100 translate-y-0 pointer-events-auto visible"
|
</div>
|
||||||
)}>
|
|
||||||
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{selectedRole === 'STUDENT' ? 'New Here?' : 'Restricted Gateway'}
|
<input required type="password" name="password" placeholder="PASSWORD" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.password} onChange={handleInputChange} />
|
||||||
</h2>
|
<input required type="password" name="confirmPassword" placeholder="CONFIRM" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.confirmPassword} onChange={handleInputChange} />
|
||||||
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
|
</div>
|
||||||
{selectedRole === 'STUDENT'
|
|
||||||
? 'Sign up and discover a world of possibilities at RIT Events Hub.'
|
<div className="grid grid-cols-2 gap-4">
|
||||||
: 'Coordinator and Admin profiles are pre-allocated. Please consult systems management for access credentials.'}
|
<select required name="gender" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all cursor-pointer" value={formData.gender} onChange={handleInputChange}>
|
||||||
</p>
|
<option value="">SELECT GENDER</option>
|
||||||
{selectedRole === 'STUDENT' && (
|
<option value="Male">MALE</option>
|
||||||
<button
|
<option value="Female">FEMALE</option>
|
||||||
onClick={() => setIsSignUp(true)}
|
<option value="Other">OTHER</option>
|
||||||
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
|
</select>
|
||||||
type="button"
|
<input required name="collegeLocation" placeholder="COLLEGE LOCATION" className="bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.collegeLocation} onChange={handleInputChange} />
|
||||||
>
|
</div>
|
||||||
Sign Up
|
|
||||||
</button>
|
<input required name="collegeName" placeholder="COLLEGE NAME" className="w-full bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all" value={formData.collegeName} onChange={handleInputChange} />
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<select required name="department" className="w-full bg-slate-50 border-transparent focus:border-brand-indigo/30 border rounded-2xl px-5 py-4 text-xs font-bold outline-none transition-all cursor-pointer appearance-none" value={formData.department} onChange={handleInputChange}>
|
||||||
|
<option value="">SELECT DEPARTMENT</option>
|
||||||
|
{EXTERNAL_DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2 border border-red-100/50">
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn(
|
||||||
|
"w-full py-5 text-white text-xs font-black rounded-2xl transition-all shadow-lg active:scale-[0.98] disabled:opacity-55 flex items-center justify-center gap-2",
|
||||||
|
RIT_BLUE,
|
||||||
|
RIT_BLUE_HOVER
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
|
{isSubmitting ? 'Registering...' : 'Sign up now...'}
|
||||||
{/* Signup Overlay Message */}
|
</button>
|
||||||
<div className={cn(
|
</form>
|
||||||
"absolute inset-0 flex flex-col items-center justify-center p-12 transition-all duration-700 delay-100",
|
|
||||||
(!isSignUp || selectedRole !== 'STUDENT') ? "opacity-0 translate-y-10 pointer-events-none invisible" : "opacity-100 translate-y-0 pointer-events-auto visible"
|
|
||||||
)}>
|
|
||||||
<h2 className="text-4xl font-black text-white uppercase tracking-tighter mb-4">Welcome <span className="text-white/70">Back!</span></h2>
|
|
||||||
<p className="text-white/80 text-sm font-medium leading-relaxed mb-10 max-w-xs mx-auto">
|
|
||||||
To keep connected with us please login with your personal credentials.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSignUp(false)}
|
|
||||||
className="px-12 py-4 border-2 border-white text-white text-[10px] font-black uppercase tracking-[0.3em] rounded-full hover:bg-white hover:text-[#004a99] transition-all active:scale-95"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* ----------------------------------------------------
|
||||||
|
MOCK GOOGLE ACCOUNT CHOOSER MODAL
|
||||||
|
---------------------------------------------------- */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showGoogleChooser && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setShowGoogleChooser(false)}
|
||||||
|
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="relative w-full max-w-md bg-white rounded-[2.5rem] premium-shadow overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="p-8 space-y-4">
|
||||||
|
<div className="flex items-center justify-between pb-4 border-b border-slate-100">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-black text-brand-navy flex items-center gap-1.5 uppercase">
|
||||||
|
<Shield className="w-5 h-5 text-brand-indigo" />
|
||||||
|
Sign In with Google
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-wider mt-0.5">Mock OAuth Provider Account Chooser</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setShowGoogleChooser(false)} className="p-2 hover:bg-slate-50 rounded-xl text-slate-400 transition-all">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-1">
|
||||||
|
{mockAccounts.map((acc) => (
|
||||||
|
<button
|
||||||
|
key={acc.email}
|
||||||
|
onClick={() => handleGoogleLoginSubmit(acc.email, acc.name.split(' (')[0])}
|
||||||
|
className="w-full text-left p-3.5 bg-slate-50 hover:bg-brand-glow border border-slate-100 hover:border-brand-indigo/20 rounded-2xl flex items-center justify-between transition-all hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-black text-brand-navy leading-none">{acc.name}</p>
|
||||||
|
<p className="text-[10px] text-slate-400 font-medium mt-1">{acc.email}</p>
|
||||||
|
</div>
|
||||||
|
<span className="px-2 py-0.5 bg-white border border-slate-100 text-[8px] font-black uppercase tracking-widest text-brand-indigo rounded-md">{acc.role}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-100 pt-4 space-y-3">
|
||||||
|
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Or enter a custom email</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="student.260099@cse.ritchennai.edu.in"
|
||||||
|
value={customGoogleEmail}
|
||||||
|
onChange={(e) => setCustomGoogleEmail(e.target.value)}
|
||||||
|
className="flex-1 bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-xs font-semibold focus:outline-none focus:border-brand-indigo/30"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGoogleLoginSubmit(customGoogleEmail, "Custom Google User")}
|
||||||
|
className="px-4 py-2.5 bg-brand-navy text-white font-black text-[9px] uppercase tracking-widest rounded-xl hover:scale-105 active:scale-95 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
OAuth Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Splash Screen Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{splashState !== 'done' && (
|
||||||
|
<motion.div
|
||||||
|
key="splash-overlay"
|
||||||
|
initial={{ opacity: 1, backgroundColor: '#004a99' }}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: splashState === 'fade-to-white' ? '#ffffff' : '#004a99'
|
||||||
|
}}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
backgroundColor: { duration: 1.5, ease: "easeInOut" },
|
||||||
|
opacity: { duration: 1.5, ease: "easeInOut" }
|
||||||
|
}}
|
||||||
|
className="fixed inset-0 flex items-center justify-center z-[200] overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Background Pattern - fade it out when transitioning to white */}
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: splashState === 'fade-to-white' ? 0 : 0.1 }}
|
||||||
|
transition={{ duration: 1.0 }}
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_white_1px,_transparent_1px)] bg-[size:30px_30px]"></div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{splashState === 'logo' && (
|
||||||
|
<motion.div
|
||||||
|
key="logo"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.1 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeInOut" }}
|
||||||
|
className="flex flex-col items-center gap-4"
|
||||||
|
>
|
||||||
|
<img src={ritLogo} alt="RIT Logo" className="h-28 w-auto object-contain drop-shadow-[0_10px_20px_rgba(255,255,255,0.15)]" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{splashState === 'text' && (
|
||||||
|
<motion.div
|
||||||
|
key="text"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeInOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-black text-white tracking-widest uppercase">
|
||||||
|
RIT EVENT HUB
|
||||||
|
</h1>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -267,10 +267,69 @@ const App: React.FC = () => {
|
|||||||
profile = externalStudent;
|
profile = externalStudent;
|
||||||
finalRole = 'STUDENT';
|
finalRole = 'STUDENT';
|
||||||
} else {
|
} 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;
|
const meta = user.user_metadata;
|
||||||
// ... (rest of metadata sync logic)
|
const emailLower = (user.email || '').trim().toLowerCase();
|
||||||
if (meta && (meta.role === 'STUDENT' || meta.regNo)) {
|
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 isExternal = meta.signUpType === 'EXTERNAL';
|
||||||
const table = isExternal ? 'externalusers' : 'Studentusers';
|
const table = isExternal ? 'externalusers' : 'Studentusers';
|
||||||
|
|
||||||
@@ -303,7 +362,7 @@ const App: React.FC = () => {
|
|||||||
finalRole = 'STUDENT';
|
finalRole = 'STUDENT';
|
||||||
} else {
|
} else {
|
||||||
console.warn("Auto-sync profile failed:", syncError?.message);
|
console.warn("Auto-sync profile failed:", syncError?.message);
|
||||||
profile = syncData; // Use metadata-based object as semi-functional fallback
|
profile = syncData;
|
||||||
finalRole = 'STUDENT';
|
finalRole = 'STUDENT';
|
||||||
}
|
}
|
||||||
} else if (meta && (meta.role === 'ADMIN' || meta.role === 'COORDINATOR')) {
|
} 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) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
handleAuthChange(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.
|
// NOTE: CreateEventForm already handles the Supabase insert.
|
||||||
// This callback only refreshes the local events list.
|
// This callback only refreshes the local events list.
|
||||||
|
|||||||
@@ -196,7 +196,30 @@ const CreateEventForm: React.FC<CreateEventFormProps> = ({ onCancel, onSuccess,
|
|||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
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);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -326,12 +349,8 @@ const CreateEventForm: React.FC<CreateEventFormProps> = ({ onCancel, onSuccess,
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let finalImageUrl = formData.image;
|
// Keep Base64 string directly in the database, ignoring Firebase/Supabase storage
|
||||||
|
const 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalLimit = parseInt(formData.maxParticipants);
|
const totalLimit = parseInt(formData.maxParticipants);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ interface LoginFormProps {
|
|||||||
onBack: () => void;
|
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 LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
||||||
const [isSignUp, setIsSignUp] = useState(false);
|
const [isSignUp, setIsSignUp] = useState(false);
|
||||||
const [signUpType, setSignUpType] = useState<'INTERNAL' | 'EXTERNAL' | null>(null);
|
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 [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
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 handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
@@ -195,7 +225,7 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
|||||||
/>
|
/>
|
||||||
{!signUpType && role === 'STUDENT' ? (
|
{!signUpType && role === 'STUDENT' ? (
|
||||||
<div className="space-y-6 text-center animate-in fade-in zoom-in-95 duration-500">
|
<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">
|
<div className="grid gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSignUpType('INTERNAL')}
|
onClick={() => setSignUpType('INTERNAL')}
|
||||||
@@ -205,20 +235,20 @@ const LoginForm: React.FC<LoginFormProps> = ({ role, onSuccess, onBack }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSignUpType('EXTERNAL')}
|
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)
|
Other College (External)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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="mb-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">
|
<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>
|
</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>
|
</div>
|
||||||
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-2">Join the RIT Excellence Hub</p>
|
||||||
</div>
|
</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>}
|
{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'}
|
{isSubmitting ? 'Initializing...' : 'Sign Up Now'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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`}>
|
<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'}
|
{isSubmitting ? 'Verifying...' : 'Sign In Now'}
|
||||||
</button>
|
</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>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
createUserWithEmailAndPassword,
|
createUserWithEmailAndPassword,
|
||||||
signOut,
|
signOut,
|
||||||
onAuthStateChanged,
|
onAuthStateChanged,
|
||||||
getUser
|
getUser,
|
||||||
|
GoogleAuthProvider,
|
||||||
|
signInWithPopup
|
||||||
} from 'firebase/auth';
|
} from 'firebase/auth';
|
||||||
import {
|
import {
|
||||||
getFirestore,
|
getFirestore,
|
||||||
@@ -318,6 +320,27 @@ class FirestoreQueryBuilder {
|
|||||||
|
|
||||||
// Map supabase.auth
|
// Map supabase.auth
|
||||||
const supabaseAuth = {
|
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) {
|
async signInWithPassword({ email, password }: any) {
|
||||||
try {
|
try {
|
||||||
const credential = await signInWithEmailAndPassword(auth, email, password);
|
const credential = await signInWithEmailAndPassword(auth, email, password);
|
||||||
|
|||||||
Reference in New Issue
Block a user