feat: add Google OAuth2 Sign-In with verified student badge for DevCollab

This commit is contained in:
Shanmuga Krishnan S M
2026-08-06 12:23:50 +05:30
parent 2be0c04b80
commit 302a39e933
13 changed files with 802 additions and 29 deletions

View File

@@ -1,13 +1,17 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X } from 'lucide-react';
import { Menu, X, LogIn, LogOut, ShieldCheck, ChevronDown } from 'lucide-react';
import { NAV_LINKS } from '@/constants';
import { useAuth } from '@/context/AuthContext';
export default function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const location = useLocation();
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
const profileRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 20);
@@ -19,6 +23,17 @@ export default function Navbar() {
setIsMobileOpen(false);
}, [location.pathname]);
// Close profile dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
setIsProfileOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const checkIsActive = (linkPath: string) => {
const [pathName, searchString] = linkPath.split('?');
if (location.pathname !== pathName) return false;
@@ -104,8 +119,111 @@ export default function Navbar() {
})}
</div>
{/* Right Side */}
<div className="flex items-center gap-3">
{/* Right Side: Auth + Mobile Toggle */}
<div className="flex items-center gap-2">
{/* Auth Section */}
{isAuthenticated && user ? (
<div className="relative" ref={profileRef}>
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-white/10 transition-all cursor-pointer"
>
{user.pictureUrl ? (
<img
src={user.pictureUrl}
alt={user.name}
className="w-8 h-8 rounded-full border-2 border-white/30 object-cover"
referrerPolicy="no-referrer"
/>
) : (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-sm">
{user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="hidden lg:flex flex-col items-start">
<span className="text-white text-[11px] font-semibold leading-tight max-w-[100px] truncate">
{user.name.split(' ')[0]}
</span>
{isVerifiedStudent && (
<span className="flex items-center gap-0.5 text-[9px] text-emerald-400 font-bold">
<ShieldCheck className="w-2.5 h-2.5" /> Verified
</span>
)}
</div>
<ChevronDown className="w-3 h-3 text-slate-400 hidden lg:block" />
</button>
{/* Profile Dropdown */}
<AnimatePresence>
{isProfileOpen && (
<motion.div
initial={{ opacity: 0, y: 8, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 top-full mt-2 w-64 bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden z-50"
>
<div className="p-4 border-b border-slate-700/60">
<div className="flex items-center gap-3">
{user.pictureUrl ? (
<img
src={user.pictureUrl}
alt={user.name}
className="w-10 h-10 rounded-full border-2 border-orange-500/40 object-cover"
referrerPolicy="no-referrer"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-lg">
{user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-bold truncate">{user.name}</p>
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
</div>
</div>
{isVerifiedStudent ? (
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-[11px] font-bold text-emerald-400">Verified RIT Student</span>
</div>
) : (
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-500/15 border border-amber-500/25">
<span className="text-[11px] font-semibold text-amber-400">
Sign in with @ritchennai.edu.in for verified badge
</span>
</div>
)}
</div>
<div className="p-2">
<button
onClick={() => {
logout();
setIsProfileOpen(false);
}}
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-red-500/15 text-xs font-semibold transition-all cursor-pointer"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={loginWithGoogle}
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl text-white text-[11px] font-semibold border border-white/15 hover:border-orange-500/40 hover:bg-white/5 transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<LogIn className="w-3.5 h-3.5" />
Sign In
</motion.button>
)}
{/* Mobile Toggle */}
<motion.button
whileTap={{ scale: 0.9 }}
@@ -148,6 +266,33 @@ export default function Navbar() {
</Link>
);
})}
{/* Mobile Auth Button */}
{!isAuthenticated ? (
<button
onClick={() => {
loginWithGoogle();
setIsMobileOpen(false);
}}
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-orange-500/10 hover:bg-orange-500/20 transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<LogIn className="w-4 h-4" />
Sign In with Google
</button>
) : (
<button
onClick={() => {
logout();
setIsMobileOpen(false);
}}
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<LogOut className="w-4 h-4" />
Sign Out ({user?.name.split(' ')[0]})
</button>
)}
</div>
</motion.div>
)}