Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ coverage/
*.tmp
*.temp
.cache/

# Uploaded user files
server/public/uploads/*
!server/public/uploads/.gitkeep
9 changes: 9 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ThemeProvider } from "./context/ThemeContext";
import TermsOfService from "./pages/TermsOfService";
import PrivacyPolicy from "./pages/PrivacyPolicy";
import NotFound from "./pages/NotFound";
import Profile from "./pages/Profile";

const PrivateRoute = ({ children }) => {
const token = localStorage.getItem("token");
Expand Down Expand Up @@ -168,6 +169,14 @@ const AppLayout = () => {
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/board/:roomId"
element={
Expand Down
48 changes: 47 additions & 1 deletion client/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ import {
FaHistory,
} from "react-icons/fa";

const getInitials = (fullName, username) => {
const name = fullName || username || "?";
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return parts[0].slice(0, 2).toUpperCase();
};

const Dashboard = () => {
const [roomId, setRoomId] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
Expand All @@ -31,9 +40,31 @@ const Dashboard = () => {
const [copiedBoardId, setCopiedBoardId] = useState(null);
const navigate = useNavigate();
const location = useLocation();
const user = JSON.parse(localStorage.getItem("user"));
const [user, setUser] = useState(() => {
return JSON.parse(localStorage.getItem("user") || "{}");
});
const isTeacher = user?.role === "teacher";

// Sync latest user profile on mount if not already cached
useEffect(() => {
if (user && (user.id || user._id) && user.username) {
return;
}

const fetchProfile = async () => {
try {
const res = await api.get("/api/auth/profile");
if (res.data && res.data.user) {
localStorage.setItem("user", JSON.stringify(res.data.user));
setUser(res.data.user);
}
} catch (err) {
console.error("Failed to sync user profile:", err);
}
};
fetchProfile();
}, []);

// Redirect admin to admin panel if they somehow reach this page
useEffect(() => {
if (user?.role === "admin") {
Expand Down Expand Up @@ -231,6 +262,21 @@ const Dashboard = () => {
</p>
</div>
<div className="flex items-center gap-3 sm:gap-6 self-end sm:self-auto">
<button
onClick={() => navigate("/profile")}
className="w-10 h-10 rounded-full overflow-hidden border border-white/10 focus:outline-none hover:border-indigo-500/50 transition-colors bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-semibold text-sm shrink-0 shadow-lg shadow-indigo-500/20 cursor-pointer"
title="View Profile"
>
{user?.profilePicture ? (
<img
src={user.profilePicture}
alt={user.username}
className="w-full h-full object-cover"
/>
) : (
getInitials(user?.fullName, user?.username)
)}
</button>
<div className="text-right hidden sm:block">
<p className="text-white font-medium">{user?.username}</p>
<p className="text-xs text-slate-500 font-mono uppercase">
Expand Down
47 changes: 38 additions & 9 deletions client/src/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
import { motion } from 'framer-motion';
Expand Down Expand Up @@ -41,7 +41,11 @@ const Login = () => {
const [loading, setLoading] = useState(false);
const [tokenClient, setTokenClient] = useState(null);

const handleGoogleLogin = async (accessToken) => {
// Use a ref so the Google OAuth callback always gets the latest handler
// without recreating the tokenClient on every render (fixes stale closure bug)
const handleGoogleLoginRef = useRef(null);

const handleGoogleLogin = useCallback(async (accessToken) => {
setError('');
setLoading(true);
try {
Expand All @@ -57,29 +61,49 @@ const Login = () => {
navigate(from);
}
} catch (err) {
setError(err.response?.data?.message || 'Google login failed');
setError(err.response?.data?.message || 'Google login failed. Please try again.');
setLoading(false);
}
};
}, [navigate, from]);

// Keep ref always pointing to the latest handler
useEffect(() => {
handleGoogleLoginRef.current = handleGoogleLogin;
}, [handleGoogleLogin]);

useEffect(() => {
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;

// Don't initialize if the client ID is the placeholder or missing
if (!clientId || clientId === 'your_google_client_id_here') {
console.warn('VITE_GOOGLE_CLIENT_ID is not configured. Google Sign-In is disabled.');
return;
}

let timer;
const initializeGoogleClient = () => {
if (window.google) {
if (window.google && window.google.accounts && window.google.accounts.oauth2) {
try {
const client = window.google.accounts.oauth2.initTokenClient({
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
client_id: clientId,
scope: 'openid email profile',
// Use ref so callback always calls the latest handler,
// avoiding the stale closure problem
callback: async (tokenResponse) => {
if (tokenResponse && tokenResponse.access_token) {
await handleGoogleLogin(tokenResponse.access_token);
await handleGoogleLoginRef.current(tokenResponse.access_token);
} else if (tokenResponse && tokenResponse.error) {
// User dismissed the popup or an error occurred
if (tokenResponse.error !== 'access_denied') {
setError(`Google sign-in error: ${tokenResponse.error}`);
}
}
},
});
setTokenClient(client);
if (timer) clearInterval(timer);
} catch (err) {
console.error("Error initializing Google OAuth client:", err);
console.error('Error initializing Google OAuth client:', err);
}
}
};
Expand All @@ -93,10 +117,15 @@ const Login = () => {
}, []);

const handleGoogleClick = () => {
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
if (!clientId || clientId === 'your_google_client_id_here') {
setError('Google Sign-In is not configured. Please set VITE_GOOGLE_CLIENT_ID in the .env file.');
return;
}
if (tokenClient) {
tokenClient.requestAccessToken();
} else {
setError("Google sign-in is not ready yet. Please try again.");
setError('Google sign-in is not ready yet. Please wait a moment and try again.');
}
};

Expand Down
Loading