Compare commits
2 Commits
3ddcff8035
...
b852018f24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b852018f24 | ||
|
|
1b698d7cca |
40
src/App.tsx
40
src/App.tsx
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthProvider } from '@/context/AuthContext';
|
import { AuthProvider } from '@/context/AuthContext';
|
||||||
|
import ProtectedRoute from '@/components/Auth/ProtectedRoute';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Home from '@/pages/Home/Home';
|
import Home from '@/pages/Home/Home';
|
||||||
import Notes from '@/pages/Notes/Notes';
|
import Notes from '@/pages/Notes/Notes';
|
||||||
@@ -48,9 +49,42 @@ function App() {
|
|||||||
<Route path="/bus-routes" element={<BusRoutes />} />
|
<Route path="/bus-routes" element={<BusRoutes />} />
|
||||||
<Route path="/faculty" element={<Faculty />} />
|
<Route path="/faculty" element={<Faculty />} />
|
||||||
<Route path="/events" element={<Events />} />
|
<Route path="/events" element={<Events />} />
|
||||||
<Route path="/community" element={<Community />} />
|
|
||||||
<Route path="/collab" element={<DevCollab />} />
|
{/* Protected Routes — Require Google Sign-In */}
|
||||||
<Route path="/leetcode" element={<LeetcodeLeaderboard />} />
|
<Route
|
||||||
|
path="/community"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
title="RIT Q&A Community"
|
||||||
|
description="Connect with fellow RIT students and ask senior helpers any question about campus life and academics."
|
||||||
|
>
|
||||||
|
<Community />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/collab"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
title="Developer Collab Hub"
|
||||||
|
description="Post open-source projects, search for co-developers, and connect with fellow programmers."
|
||||||
|
requireVerifiedStudent={true}
|
||||||
|
>
|
||||||
|
<DevCollab />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/leetcode"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
title="RIT LeetCode Leaderboard"
|
||||||
|
description="Track campus competitive programming rankings and see top RIT coders."
|
||||||
|
>
|
||||||
|
<LeetcodeLeaderboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
125
src/components/Auth/GoogleAuthModal.tsx
Normal file
125
src/components/Auth/GoogleAuthModal.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X, ShieldCheck, Sparkles } from 'lucide-react';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
|
interface GoogleAuthModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
|
||||||
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
// Close modal automatically if user gets authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated && isOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, isOpen, onClose]);
|
||||||
|
|
||||||
|
// Render Google Button inside modal when open
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const renderGoogleBtn = () => {
|
||||||
|
if (window.google && buttonRef.current) {
|
||||||
|
buttonRef.current.innerHTML = '';
|
||||||
|
window.google.accounts.id.renderButton(buttonRef.current, {
|
||||||
|
type: 'standard',
|
||||||
|
theme: 'outline',
|
||||||
|
size: 'large',
|
||||||
|
text: 'signin_with',
|
||||||
|
shape: 'pill',
|
||||||
|
logo_alignment: 'left',
|
||||||
|
width: 280,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render immediately if script is loaded
|
||||||
|
renderGoogleBtn();
|
||||||
|
|
||||||
|
// Or poll briefly if script is loading
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (window.google) {
|
||||||
|
renderGoogleBtn();
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-slate-950/70 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal Container */}
|
||||||
|
<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 }}
|
||||||
|
transition={{ type: 'spring', bounce: 0.2, duration: 0.3 }}
|
||||||
|
className="relative w-full max-w-md bg-slate-900 border border-slate-700/80 rounded-3xl shadow-2xl p-8 z-10 text-center overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Background Orbs */}
|
||||||
|
<div className="absolute -top-20 -right-20 w-40 h-40 bg-orange-500/20 rounded-full blur-2xl pointer-events-none" />
|
||||||
|
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-blue-500/20 rounded-full blur-2xl pointer-events-none" />
|
||||||
|
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-800 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Icon Header */}
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center mx-auto mb-5">
|
||||||
|
<Sparkles className="w-8 h-8 text-[#F97316]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Description */}
|
||||||
|
<h2
|
||||||
|
className="text-2xl font-bold text-white mb-2"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
Sign In to Freshers Hub
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-xs text-slate-300 mb-6 leading-relaxed"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
Sign in with your Google account. Use your{' '}
|
||||||
|
<strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email to unlock the{' '}
|
||||||
|
<strong className="text-emerald-400">Verified Student</strong> badge and post collaboration requests!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Google Sign-In Button Container */}
|
||||||
|
<div className="flex justify-center my-6 min-h-[44px]">
|
||||||
|
<div ref={buttonRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Verified Student Callout */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-center gap-2 text-[11px] text-slate-400">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>Official RIT Student Verification via Google</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
src/components/Auth/ProtectedRoute.tsx
Normal file
100
src/components/Auth/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { ShieldAlert, LogIn, Sparkles, ShieldCheck } from 'lucide-react';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
requireVerifiedStudent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProtectedRoute({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
requireVerifiedStudent = false,
|
||||||
|
}: ProtectedRouteProps) {
|
||||||
|
const { isAuthenticated, isVerifiedStudent, loginWithGoogle, user } = useAuth();
|
||||||
|
|
||||||
|
// If user is authenticated (and meets verified student criteria if required), render the actual page!
|
||||||
|
const isAllowed = isAuthenticated && (!requireVerifiedStudent || isVerifiedStudent);
|
||||||
|
|
||||||
|
if (isAllowed) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[80vh] flex items-center justify-center p-4 relative overflow-hidden bg-[#FAFAFA]">
|
||||||
|
{/* Background ambient light */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
|
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-96 h-96 bg-orange-200/30 rounded-full blur-3xl opacity-60" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="relative z-10 w-full max-w-lg bg-white border border-[#E5E7EB] rounded-3xl shadow-xl p-8 sm:p-10 text-center"
|
||||||
|
>
|
||||||
|
{/* Lock / Shield Icon */}
|
||||||
|
<div className="w-20 h-20 rounded-3xl bg-orange-50 border border-orange-200 flex items-center justify-center mx-auto mb-6 shadow-sm">
|
||||||
|
{requireVerifiedStudent ? (
|
||||||
|
<ShieldCheck className="w-10 h-10 text-[#F97316]" />
|
||||||
|
) : (
|
||||||
|
<ShieldAlert className="w-10 h-10 text-[#F97316]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature Title */}
|
||||||
|
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 text-[#F97316] text-xs font-semibold mb-3 border border-orange-200">
|
||||||
|
<Sparkles className="w-3.5 h-3.5" /> Authentication Required
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
className="text-2xl sm:text-3xl font-bold text-[#1E293B] mb-3"
|
||||||
|
style={{ fontFamily: 'Playfair Display, serif' }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-slate-600 text-sm mb-8 leading-relaxed max-w-md mx-auto"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<>
|
||||||
|
{description} Sign in with your Google account to access this feature.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
You are signed in as <strong>{user?.email}</strong>. This feature is restricted to verified students with an <strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={loginWithGoogle}
|
||||||
|
className="w-full flex items-center justify-center gap-2.5 px-6 py-4 rounded-2xl text-white font-semibold text-sm shadow-lg shadow-orange-500/25 cursor-pointer"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
fontFamily: 'Poppins, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogIn className="w-5 h-5" />
|
||||||
|
{!isAuthenticated ? 'Sign In with Google to Unlock' : 'Switch to @ritchennai.edu.in Account'}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
Sign-in is quick, secure, and preserves your student privacy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { getBackendUrl } from '@/lib/utils';
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
|
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Types
|
// Types
|
||||||
@@ -19,7 +20,10 @@ interface AuthContextType {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isVerifiedStudent: boolean;
|
isVerifiedStudent: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isAuthModalOpen: boolean;
|
||||||
loginWithGoogle: () => void;
|
loginWithGoogle: () => void;
|
||||||
|
openAuthModal: () => void;
|
||||||
|
closeAuthModal: () => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,10 +31,9 @@ interface AuthContextType {
|
|||||||
// Google Client ID
|
// Google Client ID
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
// IMPORTANT: Replace this with your actual Google Cloud Console OAuth2 Client ID
|
const GOOGLE_CLIENT_ID =
|
||||||
// Create one at: https://console.cloud.google.com/apis/credentials
|
import.meta.env.VITE_GOOGLE_CLIENT_ID ||
|
||||||
// Authorized origins: https://rit-services.in, http://localhost:5173
|
'81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com';
|
||||||
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || '';
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Context
|
// Context
|
||||||
@@ -41,7 +44,10 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isVerifiedStudent: false,
|
isVerifiedStudent: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
isAuthModalOpen: false,
|
||||||
loginWithGoogle: () => {},
|
loginWithGoogle: () => {},
|
||||||
|
openAuthModal: () => {},
|
||||||
|
closeAuthModal: () => {},
|
||||||
logout: () => {},
|
logout: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +81,10 @@ const STORAGE_KEY = 'rit_auth_user';
|
|||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
const [user, setUser] = useState<AuthUser | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
|
||||||
|
const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []);
|
||||||
|
|
||||||
// Handle the credential response from Google GSI
|
// Handle the credential response from Google GSI
|
||||||
const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
|
const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
|
||||||
@@ -93,6 +103,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const userData: AuthUser = await res.json();
|
const userData: AuthUser = await res.json();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Google auth error:', err);
|
console.error('Google auth error:', err);
|
||||||
}
|
}
|
||||||
@@ -109,17 +120,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Validate session is still valid against backend
|
// Validate session is still valid against backend
|
||||||
fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`))
|
fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`))
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// Session expired, clear it
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
} else {
|
} else {
|
||||||
// Update with fresh data
|
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(freshData => {
|
.then((freshData) => {
|
||||||
if (freshData) {
|
if (freshData) {
|
||||||
setUser(freshData);
|
setUser(freshData);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData));
|
||||||
@@ -146,7 +155,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// The GSI script may not have loaded yet (async), so poll briefly
|
|
||||||
if (window.google) {
|
if (window.google) {
|
||||||
initGSI();
|
initGSI();
|
||||||
} else {
|
} else {
|
||||||
@@ -156,57 +164,33 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
initGSI();
|
initGSI();
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
// Stop polling after 10 seconds
|
|
||||||
setTimeout(() => clearInterval(interval), 10000);
|
setTimeout(() => clearInterval(interval), 10000);
|
||||||
}
|
}
|
||||||
}, [handleCredentialResponse]);
|
}, [handleCredentialResponse]);
|
||||||
|
|
||||||
const loginWithGoogle = useCallback(() => {
|
const loginWithGoogle = useCallback(() => {
|
||||||
if (!GOOGLE_CLIENT_ID) {
|
// Open the clean Google Sign-In modal directly
|
||||||
console.warn('Google Client ID not configured. Set VITE_GOOGLE_CLIENT_ID in .env');
|
setIsAuthModalOpen(true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Also trigger One Tap prompt as a secondary background hint if supported
|
||||||
if (window.google) {
|
if (window.google) {
|
||||||
// Trigger the One Tap prompt
|
try {
|
||||||
window.google.accounts.id.prompt((notification) => {
|
window.google.accounts.id.prompt();
|
||||||
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
} catch {
|
||||||
// If One Tap doesn't show (e.g. blocked by browser), fall back to button-based flow
|
// Silently ignore if browser blocks One Tap
|
||||||
// Render a temporary popup button
|
}
|
||||||
const popup = document.createElement('div');
|
|
||||||
popup.id = 'google-signin-popup';
|
|
||||||
popup.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99999;background:white;padding:32px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);';
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
|
|
||||||
window.google!.accounts.id.renderButton(popup, {
|
|
||||||
theme: 'outline',
|
|
||||||
size: 'large',
|
|
||||||
text: 'signin_with',
|
|
||||||
shape: 'pill',
|
|
||||||
width: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove after 30s or when clicked
|
|
||||||
setTimeout(() => popup.remove(), 30000);
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (!document.getElementById('google-signin-popup')) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document.body, { childList: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
if (user?.email && window.google) {
|
if (user?.email && window.google) {
|
||||||
window.google.accounts.id.revoke(user.email, () => {
|
try {
|
||||||
console.log('Google session revoked');
|
window.google.accounts.id.revoke(user.email, () => {});
|
||||||
});
|
} catch {}
|
||||||
}
|
}
|
||||||
setUser(null);
|
setUser(null);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,11 +200,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
isAuthenticated: !!user,
|
isAuthenticated: !!user,
|
||||||
isVerifiedStudent: !!user?.verifiedStudent,
|
isVerifiedStudent: !!user?.verifiedStudent,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isAuthModalOpen,
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
|
openAuthModal,
|
||||||
|
closeAuthModal,
|
||||||
logout,
|
logout,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<GoogleAuthModal isOpen={isAuthModalOpen} onClose={closeAuthModal} />
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user