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
17 changes: 11 additions & 6 deletions backend/tests/uploadPhoto.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ import { errorHandler } from "../middlewares/errorHandler.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const profilesUploadDir = path.resolve(__dirname, "../uploads/profiles");
const profilesUploadDir = path.resolve(__dirname, "../../uploads/profiles");

// ── Supabase stub (requireAuth fast-path won't reach it, but the import needs it) ──
// Extended with a storage stub so the /api/upload suite below (uploadController.js)
// can assert on the path passed to storage.upload().
const { storageUploadMock, storageFromMock } = vi.hoisted(() => {
const storageUploadMock = vi.fn(async (_filePath, fileStream) => {
const finished = once(fileStream, "end");
fileStream.resume();
await finished;
return { data: { path: "mock-path" }, error: null };
const storageUploadMock = vi.fn((path, stream) => {
if (stream) {
if (typeof stream.on === 'function') {
stream.on('error', () => {}); // swallow unhandled stream errors in mock
}
if (typeof stream.destroy === 'function') {
stream.destroy();
}
}
return Promise.resolve({ data: { path: "mock-path" }, error: null });
});
const storageFromMock = vi.fn(() => ({
upload: storageUploadMock,
Expand Down
23 changes: 9 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, Suspense, useState, useRef } from "react";
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, Navigate, Router, useLocation } from "react-router-dom";
import { RouterProvider } from "react-router-dom";

import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
Expand All @@ -10,9 +10,6 @@ import { AuthProvider } from "@/contexts/AuthContext";
import { CookieConsentProvider } from "@/contexts/CookieConsentContext";
import { RoleProvider } from "@/contexts/RoleContext";
import { ThemeProvider } from "@/contexts/ThemeContext";
import AdminRoute from "@/components/AdminRoute";
import ProtectedRoute from "@/components/ProtectedRoute";
import ProtectedMentorRoute from "@/components/ProtectedMentorRoute";

// Global layout components - rendered on every page, keep static
import Navbar from "./components/Navbar/Navbar";
Expand Down Expand Up @@ -402,15 +399,13 @@ function App() {
<Toaster />
<Sonner />

<BrowserRouter>
<CookieConsentProvider>
<AuthProvider>
<RoleProvider>
<AppContent />
</RoleProvider>
</AuthProvider>
</CookieConsentProvider>
</BrowserRouter>
<CookieConsentProvider>
<AuthProvider>
<RoleProvider>
<RouterProvider router={router} />
</RoleProvider>
</AuthProvider>
</CookieConsentProvider>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ export const useResources = (filters?: ResourceFilters) => {
return;
}

const savedData = await safeSupabaseCall<SavedResource[]>(
const savedData = await safeSupabaseCall(
() => (supabase as any).from("saved_resources").select("resource_id").eq("user_id", user.id).abortSignal(controller.signal)
);

savedResourceIds =
(savedData as SavedResource[] | null)?.map(
(item) => item.resource_id
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useSkillEndorsements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
}).catch(console.error);

// Subscribe to changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {

Check failure on line 49 in src/hooks/useSkillEndorsements.ts

View workflow job for this annotation

GitHub Actions / test

[frontend] src/hooks/useSkillEndorsements.test.ts > useSkillEndorsements rapid interactions > prevents rapid toggling state desync

TypeError: supabase.auth.onAuthStateChange is not a function ❯ src/hooks/useSkillEndorsements.ts:49:54 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24
if (mounted) setCurrentUserId(session?.user?.id ?? null);
});

Expand All @@ -64,7 +64,7 @@
}

try {
const { data, error } = await supabase
const { data, error } = await (supabase as any)
.from("skill_endorsements")
.select("skill, endorser_id")
.eq("endorsed_user_id", profileUserId)
Expand Down Expand Up @@ -133,7 +133,7 @@

try {
if (isRemoving) {
const { error } = await supabase
const { error } = await (supabase as any)
.from("skill_endorsements")
.delete()
.match({
Expand All @@ -143,7 +143,7 @@
});
if (error) throw error;
} else {
const { error } = await supabase
const { error } = await (supabase as any)
.from("skill_endorsements")
.insert({
skill,
Expand Down
11 changes: 11 additions & 0 deletions src/layouts/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import AdminRoute from "@/components/AdminRoute";
import MainLayout from "./MainLayout";

export default function AdminLayout() {
return (
<AdminRoute>
<MainLayout />
</AdminRoute>
);
}
16 changes: 16 additions & 0 deletions src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import { Outlet } from "react-router-dom";
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap <Outlet /> with <Suspense> to handle lazy-loaded routes without unmounting the navigation.

The feature route modules use React.lazy() for page components. Without a <Suspense> boundary inside MainLayout, navigating to a lazy-loaded route will suspend the layout hierarchy up to the nearest root boundary (likely in App.tsx), causing the Navbar to completely unmount and flash during page transitions.

Wrapping <Outlet /> ensures the navigation UI remains visible while the lazy page loads.

🛠️ Proposed fix
-import React from "react";
+import React, { Suspense } from "react";
 import { Outlet } from "react-router-dom";
 
 // ... existing imports ...
 
 export default function MainLayout() {
   const { user } = useAuth();
   return (
     <>
       <Navbar />
       {user && <StreakBadge />}
-      <Outlet />
+      <Suspense fallback={
+        <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
+          <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
+        </div>
+      }>
+        <Outlet />
+      </Suspense>
     </>
   );
 }

Also applies to: 10-14

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/layouts/MainLayout.tsx` around lines 1 - 2, Update MainLayout to import
Suspense and wrap the Outlet component with a Suspense boundary, using the
existing or appropriate loading fallback. Keep Navbar outside this boundary so
it remains mounted during lazy route transitions.

import Navbar from "@/components/Navbar/Navbar";
import StreakBadge from "@/components/StreakBadge";
import { useAuth } from "@/contexts/useAuth";

export default function MainLayout() {
const { user } = useAuth();
return (
<>
<Navbar />
{user && <StreakBadge />}
<Outlet />
</>
);
}
11 changes: 11 additions & 0 deletions src/layouts/ProtectedLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import ProtectedRoute from "@/components/ProtectedRoute";
import MainLayout from "./MainLayout";

export default function ProtectedLayout() {
return (
<ProtectedRoute>
<MainLayout />
</ProtectedRoute>
Comment on lines +7 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Prevent layout remounts and duplicate API calls by centralizing MainLayout.

Because <MainLayout /> is independently defined as a nested element inside ProtectedLayout, ProtectedMentorLayout, AdminLayout, and the publicRoutes configuration, React Router evaluates each as a distinct layout branch. When a user navigates across these boundaries (e.g., from / to /dashboard, or /dashboard to /mentor-dashboard), React Router fully unmounts and remounts MainLayout. This destroys local Navbar state and triggers StreakBadge to unnecessarily re-fetch data from the backend on every layout transition.

To fix this, render <Outlet /> inside these layout guards instead of <MainLayout />. Then, in your central router configuration (e.g., src/router/index.tsx), wrap the unified route tree that requires the global shell with a single, top-level <MainLayout /> parent route.

  • src/layouts/ProtectedLayout.tsx#L7-L9: Replace <MainLayout /> with <Outlet /> (ensure Outlet is imported from react-router-dom).
  • src/layouts/ProtectedMentorLayout.tsx#L7-L9: Replace <MainLayout /> with <Outlet /> (ensure Outlet is imported).
  • src/layouts/AdminLayout.tsx#L7-L9: Replace <MainLayout /> with <Outlet /> (ensure Outlet is imported).
  • src/router/public.routes.tsx#L19-L28: Remove the <MainLayout /> parent object and directly export the flat array of public routes, so they can be nested under the central <MainLayout /> in your router configuration.
📍 Affects 4 files
  • src/layouts/ProtectedLayout.tsx#L7-L9 (this comment)
  • src/layouts/ProtectedMentorLayout.tsx#L7-L9
  • src/layouts/AdminLayout.tsx#L7-L9
  • src/router/public.routes.tsx#L19-L28
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/layouts/ProtectedLayout.tsx` around lines 7 - 9, Centralize MainLayout to
prevent remounts across route branches. In
src/layouts/ProtectedLayout.tsx#L7-L9,
src/layouts/ProtectedMentorLayout.tsx#L7-L9, and
src/layouts/AdminLayout.tsx#L7-L9, replace MainLayout with Outlet and import
Outlet from react-router-dom. In src/router/public.routes.tsx#L19-L28, remove
the MainLayout parent route and export the public routes as a flat array; nest
the unified route tree under one top-level MainLayout parent in the central
router configuration.

);
}
11 changes: 11 additions & 0 deletions src/layouts/ProtectedMentorLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import ProtectedMentorRoute from "@/components/ProtectedMentorRoute";
import MainLayout from "./MainLayout";

export default function ProtectedMentorLayout() {
return (
<ProtectedMentorRoute>
<MainLayout />
</ProtectedMentorRoute>
);
}
50 changes: 50 additions & 0 deletions src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useState, useEffect, Suspense } from "react";
import { Outlet } from "react-router-dom";
import { useAuth } from "@/contexts/useAuth";

import SplashScreen from "@/components/SplashScreen";
import MouseSparkles from "@/components/MouseSparkles";
import CookieConsentBanner from "@/components/CookieConsentBanner";
import Chatbot from "@/components/Chatbot/Chatbot";
import FloatingAI from "@/components/FloatingAI";
import BackToTop from "@/components/BackToTop";

export default function RootLayout() {
const { user } = useAuth();
const [loading, setLoading] = useState(true);

useEffect(() => {
const timer = setTimeout(() => setLoading(false), 2500);
return () => clearTimeout(timer);
}, []);

if (loading) {
return <SplashScreen />;
}

return (
<>
<MouseSparkles />
<CookieConsentBanner />

<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-[#020617]">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-cyan-400 border-t-transparent" />
</div>
}
>
<Outlet />
</Suspense>

{user && (
<>
<Chatbot />
<FloatingAI />
</>
)}

<BackToTop />
</>
);
}
4 changes: 3 additions & 1 deletion src/pages/Contact.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ describe("Contact", () => {

beforeEach(() => {
vi.clearAllMocks();
localStorage?.clear();
if (typeof window !== "undefined" && window.localStorage && typeof window.localStorage.clear === "function") {
window.localStorage.clear();
}

(useToast as any).mockReturnValue({ toast });
(supabase.from as any).mockReturnValue({ insert });
Expand Down
14 changes: 14 additions & 0 deletions src/router/admin.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { RouteObject } from "react-router-dom";
import AdminLayout from "@/layouts/AdminLayout";

const Admin = React.lazy(() => import("@/pages/Admin"));

export const adminRoutes: RouteObject[] = [
{
element: <AdminLayout />,
children: [
{ path: "/admin", element: <Admin /> },
],
},
];
22 changes: 22 additions & 0 deletions src/router/auth.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { RouteObject } from "react-router-dom";

const Login = React.lazy(() => import("@/pages/Login"));
const Signup = React.lazy(() => import("@/pages/Signup"));
const ForgotPassword = React.lazy(() => import("@/pages/ForgotPassword"));
const ResetPassword = React.lazy(() => import("@/pages/ResetPassword"));
const AuthCallback = React.lazy(() => import("@/pages/AuthCallback"));
const Onboarding = React.lazy(() => import("@/pages/Onboarding"));
const PublicPortfolio = React.lazy(() => import("@/pages/PublicPortfolio"));
const BecomeMentor = React.lazy(() => import("@/pages/BecomeMentor"));

export const authRoutes: RouteObject[] = [
{ path: "/login", element: <Login /> },
{ path: "/signup", element: <Signup /> },
{ path: "/forgot-password", element: <ForgotPassword /> },
{ path: "/reset-password", element: <ResetPassword /> },
{ path: "/auth/callback", element: <AuthCallback /> },
{ path: "/onboarding", element: <Onboarding /> },
{ path: "/portfolio/:slug", element: <PublicPortfolio /> },
{ path: "/become-mentor", element: <BecomeMentor /> },
];
23 changes: 23 additions & 0 deletions src/router/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import RootLayout from "@/layouts/RootLayout";
import { publicRoutes } from "./public.routes";
import { authRoutes } from "./auth.routes";
import { protectedRoutes } from "./protected.routes";
import { settingsRoutes } from "./settings.routes";
import { mentorRoutes } from "./mentor.routes";
import { adminRoutes } from "./admin.routes";

export const router = createBrowserRouter([
{
element: <RootLayout />,
children: [
...publicRoutes,
...authRoutes,
...protectedRoutes,
...settingsRoutes,
...mentorRoutes,
...adminRoutes,
],
},
]);
14 changes: 14 additions & 0 deletions src/router/mentor.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { RouteObject } from "react-router-dom";
import ProtectedMentorLayout from "@/layouts/ProtectedMentorLayout";

const MentorDashboard = React.lazy(() => import("@/pages/MentorDashboard"));

export const mentorRoutes: RouteObject[] = [
{
element: <ProtectedMentorLayout />,
children: [
{ path: "/mentor-dashboard", element: <MentorDashboard /> },
],
},
];
56 changes: 56 additions & 0 deletions src/router/protected.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";
import { RouteObject } from "react-router-dom";
import ProtectedLayout from "@/layouts/ProtectedLayout";
import { useAuth } from "@/contexts/useAuth";

const Dashboard = React.lazy(() => import("@/pages/Dashboard"));
const LearnerDashboard = React.lazy(() => import("@/pages/LearnerDashboard"));
const Discover = React.lazy(() => import("@/pages/Discover"));
const Sessions = React.lazy(() => import("@/pages/Sessions"));
const Messages = React.lazy(() => import("@/pages/Messages"));
const Chat = React.lazy(() => import("@/pages/Chat"));
const Notifications = React.lazy(() => import("@/pages/Notifications"));
const Leaderboard = React.lazy(() => import("@/pages/Leaderboard"));
const ResourceHub = React.lazy(() => import("@/pages/ResourceHub"));
const Portfolio = React.lazy(() => import("@/pages/Portfolio"));
const PeerReviewDashboard = React.lazy(() => import("@/pages/PeerReviewDashboard"));
const SubmitForReview = React.lazy(() => import("@/pages/SubmitForReview"));
const ReviewSubmission = React.lazy(() => import("@/pages/ReviewSubmission"));
const MockInterview = React.lazy(() => import("@/pages/MockInterview"));
const AnonymousDoubts = React.lazy(() => import("@/pages/AnonymousDoubts"));
const ContributorDashboard = React.lazy(() => import("@/pages/ContributorDashboard"));
const AIPage = React.lazy(() => import("@/pages/aipage"));
const StudyRooms = React.lazy(() => import("@/components/StudyRooms"));
const Room = React.lazy(() => import("@/components/Room/Room"));

const MessagesRoute = () => {

Check warning on line 26 in src/router/protected.routes.tsx

View workflow job for this annotation

GitHub Actions / test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file
const { user } = useAuth();
return <Messages user={user} />;
};

export const protectedRoutes: RouteObject[] = [
{
element: <ProtectedLayout />,
children: [
{ path: "/dashboard", element: <Dashboard /> },
{ path: "/learner-dashboard", element: <LearnerDashboard /> },
{ path: "/discover", element: <Discover /> },
{ path: "/sessions", element: <Sessions /> },
{ path: "/messages", element: <MessagesRoute /> },
{ path: "/chat", element: <Chat /> },
{ path: "/notifications", element: <Notifications /> },
{ path: "/leaderboard", element: <Leaderboard /> },
{ path: "/resources", element: <ResourceHub /> },
{ path: "/portfolio", element: <Portfolio /> },
{ path: "/peer-review", element: <PeerReviewDashboard /> },
{ path: "/peer-review/new", element: <SubmitForReview /> },
{ path: "/peer-review/:id", element: <ReviewSubmission /> },
{ path: "/mock-interview", element: <MockInterview /> },
{ path: "/anonymous-doubts", element: <AnonymousDoubts /> },
{ path: "/contributor-dashboard", element: <ContributorDashboard /> },
{ path: "/ai", element: <AIPage /> },
{ path: "/rooms", element: <StudyRooms /> },
{ path: "/rooms/:id", element: <Room /> },
],
},
];
33 changes: 33 additions & 0 deletions src/router/public.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { Navigate, RouteObject } from "react-router-dom";
import MainLayout from "@/layouts/MainLayout";
import { useAuth } from "@/contexts/useAuth";

const Index = React.lazy(() => import("@/pages/Index"));
const Contact = React.lazy(() => import("@/pages/Contact"));
const PrivacyPolicy = React.lazy(() => import("@/pages/privacy"));
const CookiesPolicy = React.lazy(() => import("@/pages/cookies-policy"));
const TermsAndConditions = React.lazy(() => import("@/pages/TermsAndConditions"));
const NotFound = React.lazy(() => import("@/pages/NotFound"));

const IndexRoute = () => {

Check warning on line 13 in src/router/public.routes.tsx

View workflow job for this annotation

GitHub Actions / test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file
const { user } = useAuth();
return user ? <Navigate to="/dashboard" replace /> : <Index />;
};
Comment on lines +13 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle the loading state to prevent unauthenticated UI flashing.

Because useAuth() loads asynchronously, user is initially null. When an authenticated user visits /, the application will briefly render the <Index /> page before user is populated and triggers the redirect to /dashboard.

Check the loading state and render a loading spinner until the authentication resolution is complete.

🛠️ Proposed fix
 const IndexRoute = () => {
-  const { user } = useAuth();
-  return user ? <Navigate to="/dashboard" replace /> : <Index />;
+  const { user, loading } = useAuth();
+  
+  if (loading) {
+    return (
+      <div className="flex min-h-screen items-center justify-center">
+        <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
+      </div>
+    );
+  }
+  
+  return user ? <Navigate to="/dashboard" replace /> : <Index />;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const IndexRoute = () => {
const { user } = useAuth();
return user ? <Navigate to="/dashboard" replace /> : <Index />;
};
const IndexRoute = () => {
const { user, loading } = useAuth();
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
);
}
return user ? <Navigate to="/dashboard" replace /> : <Index />;
};
🧰 Tools
🪛 GitHub Check: test

[warning] 13-13:
Fast refresh only works when a file only exports components. Move your component(s) to a separate file

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/router/public.routes.tsx` around lines 13 - 16, Update IndexRoute to use
the loading state returned by useAuth(); render the existing loading spinner
while authentication is unresolved, then preserve the current user-based
redirect to /dashboard or Index page once loading completes.


export const publicRoutes: RouteObject[] = [
{
element: <MainLayout />,
children: [
{ path: "/", element: <IndexRoute /> },
{ path: "/contact", element: <Contact /> },
{ path: "/privacy-policy", element: <PrivacyPolicy /> },
{ path: "/cookies-policy", element: <CookiesPolicy /> },
{ path: "/terms-and-conditions", element: <TermsAndConditions /> },
],
},
{
path: "*",
element: <NotFound />,
},
];
Loading
Loading