diff --git a/client/package-lock.json b/client/package-lock.json
index 24ca143..8c12b36 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2284,7 +2284,7 @@
"version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
- "dev": true,
+ "devOptional": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -2385,6 +2385,18 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -2828,6 +2840,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
@@ -3375,6 +3393,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4330,6 +4357,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
@@ -4360,6 +4434,28 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/recharts": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.0.2.tgz",
@@ -4729,6 +4825,49 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
diff --git a/client/src/components/Header.jsx b/client/src/components/Header.jsx
index 71b4e90..a09c072 100644
--- a/client/src/components/Header.jsx
+++ b/client/src/components/Header.jsx
@@ -1,60 +1,65 @@
-import { useState, useRef, useEffect } from "react"
-import { Link } from "react-router-dom"
-import { FaUser, FaSignOutAlt, FaChevronDown, FaBars, FaTimes } from "react-icons/fa"
-import { useAuth } from "../context/AuthContext"
-import React from "react"
+import { useState, useRef, useEffect } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { FaUser, FaSignOutAlt, FaChevronDown, FaBars, FaTimes } from "react-icons/fa";
+import { useAuth } from "../context/AuthContext";
+import React from "react";
export default function Header() {
- const { user, isLoggedIn, logout } = useAuth()
- const [isDropdownOpen, setIsDropdownOpen] = useState(false)
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
- const [showLogoutConfirm, setShowLogoutConfirm] = useState(false)
- const dropdownRef = useRef(null)
+ const { user, isLoggedIn, logout } = useAuth();
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
+ const dropdownRef = useRef(null);
+ const navigate = useNavigate();
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
- setIsDropdownOpen(false)
+ setIsDropdownOpen(false);
}
}
- document.addEventListener("mousedown", handleClickOutside)
+ document.addEventListener("mousedown", handleClickOutside);
return () => {
- document.removeEventListener("mousedown", handleClickOutside)
- }
- }, [])
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, []);
const handleLogout = () => {
- setShowLogoutConfirm(true)
- setIsDropdownOpen(false)
- }
+ setShowLogoutConfirm(true);
+ setIsDropdownOpen(false);
+ };
+
+ const confirmLogout = async () => {
+ // If you have a delete endpoint, replace this with axios call
+ // await axios.delete("/api/user/delete", { headers: { Authorization: `Bearer ${user.token}` } });
- const confirmLogout = () => {
- logout()
- setShowLogoutConfirm(false)
- }
+ logout(); // Clears auth state
+ setShowLogoutConfirm(false);
+ navigate("/login");
+ };
const cancelLogout = () => {
- setShowLogoutConfirm(false)
- }
+ setShowLogoutConfirm(false);
+ };
const toggleDropdown = () => {
- setIsDropdownOpen(!isDropdownOpen)
- }
+ setIsDropdownOpen(!isDropdownOpen);
+ };
const toggleMobileMenu = () => {
- setIsMobileMenuOpen(!isMobileMenuOpen)
- }
+ setIsMobileMenuOpen(!isMobileMenuOpen);
+ };
const getInitials = (name) => {
- if (!name) return "U"
+ if (!name) return "U";
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
- .slice(0, 2)
- }
+ .slice(0, 2);
+ };
const NavLinks = () => (
@@ -68,7 +73,7 @@ export default function Header() {
)}
Resources
- )
+ );
return (
<>
@@ -96,7 +101,7 @@ export default function Header() {
- {!isLoggedIn ? (
+ {!user || !isLoggedIn ? (
<>
Log in
Sign up
@@ -144,7 +149,7 @@ export default function Header() {
{isMobileMenuOpen && (
- {!isLoggedIn ? (
+ {!user || !isLoggedIn ? (
Log in
Sign up
@@ -183,5 +188,5 @@ export default function Header() {
)}
>
- )
-}
+ );
+}
\ No newline at end of file
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx
index 4700a70..6c63428 100644
--- a/client/src/context/AuthContext.jsx
+++ b/client/src/context/AuthContext.jsx
@@ -11,6 +11,20 @@ export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
+ const refreshUser = async () => {
+ const current = auth.currentUser;
+ if (current) {
+ const token = await current.getIdToken();
+ setUser({
+ uid: current.uid,
+ email: current.email,
+ name: current.displayName || "",
+ avatar: current.photoURL || "",
+ token,
+ });
+ }
+ };
+
const logout = () => {
auth.signOut()
.then(() => {
@@ -25,7 +39,16 @@ export const AuthProvider = ({ children }) => {
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
if (firebaseUser) {
- setUser(firebaseUser);
+ const token = await firebaseUser.getIdToken();
+
+ setUser({
+ uid: firebaseUser.uid,
+ email: firebaseUser.email,
+ name: firebaseUser.displayName || "",
+ avatar: firebaseUser.photoURL || "",
+ token,
+ });
+
setIsLoggedIn(true);
} else {
setUser(null);
@@ -38,8 +61,19 @@ export const AuthProvider = ({ children }) => {
}, []);
return (
-
+
{children}
);
-};
+};
\ No newline at end of file
diff --git a/client/src/pages/ProfilePage.jsx b/client/src/pages/ProfilePage.jsx
new file mode 100644
index 0000000..0831435
--- /dev/null
+++ b/client/src/pages/ProfilePage.jsx
@@ -0,0 +1,205 @@
+import { useState } from "react";
+import { useAuth } from "../context/AuthContext";
+import axios from "axios";
+import Header from "../components/Header";
+import Footer from "../components/Footer";
+import { FaPen } from "react-icons/fa";
+import { useNavigate } from "react-router-dom";
+
+export default function ProfilePage() {
+ const { user, isLoggedIn, refreshUser } = useAuth();
+ const [name, setName] = useState(user?.name || "");
+ const [email] = useState(user?.email || "");
+ const [editName, setEditName] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [deleting, setDeleting] = useState(false);
+ const [message, setMessage] = useState("");
+ const [error, setError] = useState("");
+ const navigate = useNavigate();
+
+ const isEdited = editName && name.trim() !== user.name;
+
+ const recentActivity = [
+ { id: 1, action: "Completed React quiz", date: "Aug 1, 2025" },
+ { id: 2, action: "Commented on State Management article", date: "Jul 30, 2025" },
+ { id: 3, action: "Joined Practice Room", date: "Jul 28, 2025" },
+ ];
+
+ const preferences = {
+ theme: "Light Mode",
+ notifications: true,
+ language: "English",
+ };
+
+ const updateProfile = async () => {
+ setSaving(true);
+ setMessage("");
+ setError("");
+
+ try {
+ await axios.put(`${import.meta.env.VITE_API_URL}/api/auth/update-profile`, { name }, {
+ headers: { Authorization: `Bearer ${user.token}` },
+ withCredentials: true,
+ });
+
+ await refreshUser();
+ setMessage("Profile updated successfully.");
+ setEditName(false);
+ } catch (err) {
+ console.error(err);
+ setError("Failed to update profile.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const deleteAccount = async () => {
+ if (!window.confirm("Are you sure you want to delete your account? This action is irreversible.")) return;
+
+ setDeleting(true);
+ setMessage("");
+ setError("");
+
+ try {
+ await axios.delete(`${import.meta.env.VITE_API_URL}/api/auth/delete`, {
+ headers: { Authorization: `Bearer ${user.token}` },
+ withCredentials: true,
+ });
+
+ setMessage("Account deleted. Redirecting...");
+ setTimeout(() => navigate("/login"), 1500);
+ } catch (err) {
+ console.error(err);
+ setError("Account deletion failed.");
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ if (!isLoggedIn || !user) {
+ return (
+
+
+
Please log in to view your profile.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Your Profile
+
+ {/* Avatar */}
+
+ {user.avatar ? (
+

+ ) : (
+
+ {user.name?.charAt(0) || "U"}
+
+ )}
+
+
+ {/* Editable Info */}
+
+ {/* Name */}
+
+
+ {!editName ? (
+
+
{name}
+
+
+ ) : (
+
setName(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring focus:ring-indigo-300"
+ />
+ )}
+
+
+ {/* Email */}
+
+
+ {/* Preferences & Activity */}
+
+
+
Preferences
+
+ - 🌈 Theme: {preferences.theme}
+ - 🔔 Notifications: {preferences.notifications ? "Enabled" : "Disabled"}
+ - 🗣️ Language: {preferences.language}
+
+
+
+
+
Recent Activity
+
+ {recentActivity.map((item) => (
+ -
+ {item.action}
+ {item.date}
+
+ ))}
+
+
+
+
+
+ {/* Buttons */}
+
+ {isEdited && (
+
+ )}
+
+
+
+ {/* Feedback */}
+ {(message || error) && (
+
+ {message &&
{message}
}
+ {error &&
{error}
}
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/routes.jsx b/client/src/routes.jsx
index 279c579..c4ddb04 100644
--- a/client/src/routes.jsx
+++ b/client/src/routes.jsx
@@ -11,6 +11,7 @@ import Resources from "./pages/Resources";
import About from "./pages/About";
import Contact from "./pages/Contact";
import Error404 from "./pages/404";
+import ProfilePage from "./pages/ProfilePage";
const router = createBrowserRouter([
{ path: "/", element: },
@@ -23,6 +24,7 @@ const router = createBrowserRouter([
{ path: "/resources", element: },
{ path: "/about", element: },
{ path: "/contact", element: },
+ { path: "/profile", element: },
{ path: "*", element: },
]);
diff --git a/server/config/db.js b/server/config/db.js
index 0b54af6..2d37000 100644
--- a/server/config/db.js
+++ b/server/config/db.js
@@ -2,10 +2,7 @@ import mongoose from 'mongoose';
const connectDB = async() => {
try {
- await mongoose.connect(process.env.MONGO_URL, {
- useNewUrlParser: true,
- useUnifiedTopology: true,
- });
+ await mongoose.connect(process.env.MONGO_URL);
} catch (error) {
process.exit(1);
}
diff --git a/server/index.js b/server/index.js
index 016aa75..88bff24 100644
--- a/server/index.js
+++ b/server/index.js
@@ -13,8 +13,19 @@ import multer from "multer";
dotenv.config();
const app = express();
const upload = multer({ dest: "uploads/" });
+const allowedOrigins = [
+ 'http://localhost:5173',
+ 'https://prepedgeai.vercel.app'
+];
+
app.use(cors({
- origin: 'https://prepedgeai.vercel.app',
+ origin: function (origin, callback) {
+ if (!origin || allowedOrigins.includes(origin)) {
+ callback(null, true);
+ } else {
+ callback(new Error('Not allowed by CORS'));
+ }
+ },
credentials: true,
}));
app.use(express.json());
diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js
index 262ebbf..797dc16 100644
--- a/server/routes/authRoutes.js
+++ b/server/routes/authRoutes.js
@@ -6,6 +6,7 @@
import express from "express";
import User from "../models/UserModel.js";
import firebaseAuthMiddleware from "../middleware/firebaseAuthMiddleware.js";
+import admin from "firebase-admin";
const router = express.Router();
router.use(express.json());
@@ -44,4 +45,48 @@ router.post("/login", firebaseAuthMiddleware, async (req, res) => {
}
});
+router.put("/update-profile", firebaseAuthMiddleware, async (req, res) => {
+ const { uid } = req.firebaseUser;
+ const { name, avatar } = req.body;
+
+ try {
+ const user = await User.findOne({ firebase_user_id: uid });
+ if (!user) return res.status(404).json({ error: "User not found" });
+
+ if (name) user.name = name;
+ if (avatar) user.avatar = avatar;
+
+ await user.save();
+ res.status(200).json({ message: "Profile updated successfully", user });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+router.delete("/delete", firebaseAuthMiddleware, async (req, res) => {
+ const { uid } = req.firebaseUser;
+
+ try {
+ const user = await User.findOneAndDelete({ firebase_user_id: uid });
+ if (!user) return res.status(404).json({ error: "User not found" });
+ await admin.auth().deleteUser(uid); // Delete Firebase user
+ res.status(200).json({ message: "User deleted successfully" });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+router.get("/me", firebaseAuthMiddleware, async (req, res) => {
+ const { uid } = req.firebaseUser;
+
+ try {
+ const user = await User.findOne({ firebase_user_id: uid });
+ if (!user) return res.status(404).json({ error: "User not found" });
+
+ res.status(200).json({ user });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
export default router;