diff --git a/backend/tests/uploadPhoto.test.js b/backend/tests/uploadPhoto.test.js index 5d4cc9dd..50b55625 100644 --- a/backend/tests/uploadPhoto.test.js +++ b/backend/tests/uploadPhoto.test.js @@ -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, diff --git a/src/App.tsx b/src/App.tsx index ef7a46e9..2eea7ff2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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"; @@ -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"; @@ -402,15 +399,13 @@ function App() { - - - - - - - - - + + + + + + + diff --git a/src/hooks/useResources.ts b/src/hooks/useResources.ts index 443e0761..b64d9b5c 100644 --- a/src/hooks/useResources.ts +++ b/src/hooks/useResources.ts @@ -63,10 +63,10 @@ export const useResources = (filters?: ResourceFilters) => { return; } - const savedData = await safeSupabaseCall( + 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 diff --git a/src/hooks/useSkillEndorsements.ts b/src/hooks/useSkillEndorsements.ts index af8f000b..9d40ad61 100644 --- a/src/hooks/useSkillEndorsements.ts +++ b/src/hooks/useSkillEndorsements.ts @@ -64,7 +64,7 @@ export function useSkillEndorsements({ } 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) @@ -133,7 +133,7 @@ export function useSkillEndorsements({ try { if (isRemoving) { - const { error } = await supabase + const { error } = await (supabase as any) .from("skill_endorsements") .delete() .match({ @@ -143,7 +143,7 @@ export function useSkillEndorsements({ }); if (error) throw error; } else { - const { error } = await supabase + const { error } = await (supabase as any) .from("skill_endorsements") .insert({ skill, diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx new file mode 100644 index 00000000..d76b2208 --- /dev/null +++ b/src/layouts/AdminLayout.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import AdminRoute from "@/components/AdminRoute"; +import MainLayout from "./MainLayout"; + +export default function AdminLayout() { + return ( + + + + ); +} diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx new file mode 100644 index 00000000..fb536b81 --- /dev/null +++ b/src/layouts/MainLayout.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Outlet } from "react-router-dom"; +import Navbar from "@/components/Navbar/Navbar"; +import StreakBadge from "@/components/StreakBadge"; +import { useAuth } from "@/contexts/useAuth"; + +export default function MainLayout() { + const { user } = useAuth(); + return ( + <> + + {user && } + + + ); +} diff --git a/src/layouts/ProtectedLayout.tsx b/src/layouts/ProtectedLayout.tsx new file mode 100644 index 00000000..ab2f2c94 --- /dev/null +++ b/src/layouts/ProtectedLayout.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ProtectedRoute from "@/components/ProtectedRoute"; +import MainLayout from "./MainLayout"; + +export default function ProtectedLayout() { + return ( + + + + ); +} diff --git a/src/layouts/ProtectedMentorLayout.tsx b/src/layouts/ProtectedMentorLayout.tsx new file mode 100644 index 00000000..b5507c3a --- /dev/null +++ b/src/layouts/ProtectedMentorLayout.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ProtectedMentorRoute from "@/components/ProtectedMentorRoute"; +import MainLayout from "./MainLayout"; + +export default function ProtectedMentorLayout() { + return ( + + + + ); +} diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx new file mode 100644 index 00000000..4d94a018 --- /dev/null +++ b/src/layouts/RootLayout.tsx @@ -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 ; + } + + return ( + <> + + + + +
+
+ } + > + +
+ + {user && ( + <> + + + + )} + + + + ); +} diff --git a/src/pages/Contact.test.tsx b/src/pages/Contact.test.tsx index ca4459e0..07fa0899 100644 --- a/src/pages/Contact.test.tsx +++ b/src/pages/Contact.test.tsx @@ -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 }); diff --git a/src/router/admin.routes.tsx b/src/router/admin.routes.tsx new file mode 100644 index 00000000..bcf53a48 --- /dev/null +++ b/src/router/admin.routes.tsx @@ -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: , + children: [ + { path: "/admin", element: }, + ], + }, +]; diff --git a/src/router/auth.routes.tsx b/src/router/auth.routes.tsx new file mode 100644 index 00000000..9cc45f71 --- /dev/null +++ b/src/router/auth.routes.tsx @@ -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: }, + { path: "/signup", element: }, + { path: "/forgot-password", element: }, + { path: "/reset-password", element: }, + { path: "/auth/callback", element: }, + { path: "/onboarding", element: }, + { path: "/portfolio/:slug", element: }, + { path: "/become-mentor", element: }, +]; diff --git a/src/router/index.tsx b/src/router/index.tsx new file mode 100644 index 00000000..0c59fa49 --- /dev/null +++ b/src/router/index.tsx @@ -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: , + children: [ + ...publicRoutes, + ...authRoutes, + ...protectedRoutes, + ...settingsRoutes, + ...mentorRoutes, + ...adminRoutes, + ], + }, +]); diff --git a/src/router/mentor.routes.tsx b/src/router/mentor.routes.tsx new file mode 100644 index 00000000..018d9a7d --- /dev/null +++ b/src/router/mentor.routes.tsx @@ -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: , + children: [ + { path: "/mentor-dashboard", element: }, + ], + }, +]; diff --git a/src/router/protected.routes.tsx b/src/router/protected.routes.tsx new file mode 100644 index 00000000..466fc3e4 --- /dev/null +++ b/src/router/protected.routes.tsx @@ -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 = () => { + const { user } = useAuth(); + return ; +}; + +export const protectedRoutes: RouteObject[] = [ + { + element: , + children: [ + { path: "/dashboard", element: }, + { path: "/learner-dashboard", element: }, + { path: "/discover", element: }, + { path: "/sessions", element: }, + { path: "/messages", element: }, + { path: "/chat", element: }, + { path: "/notifications", element: }, + { path: "/leaderboard", element: }, + { path: "/resources", element: }, + { path: "/portfolio", element: }, + { path: "/peer-review", element: }, + { path: "/peer-review/new", element: }, + { path: "/peer-review/:id", element: }, + { path: "/mock-interview", element: }, + { path: "/anonymous-doubts", element: }, + { path: "/contributor-dashboard", element: }, + { path: "/ai", element: }, + { path: "/rooms", element: }, + { path: "/rooms/:id", element: }, + ], + }, +]; diff --git a/src/router/public.routes.tsx b/src/router/public.routes.tsx new file mode 100644 index 00000000..92b9769f --- /dev/null +++ b/src/router/public.routes.tsx @@ -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 = () => { + const { user } = useAuth(); + return user ? : ; +}; + +export const publicRoutes: RouteObject[] = [ + { + element: , + children: [ + { path: "/", element: }, + { path: "/contact", element: }, + { path: "/privacy-policy", element: }, + { path: "/cookies-policy", element: }, + { path: "/terms-and-conditions", element: }, + ], + }, + { + path: "*", + element: , + }, +]; diff --git a/src/router/settings.routes.tsx b/src/router/settings.routes.tsx new file mode 100644 index 00000000..62c55e4f --- /dev/null +++ b/src/router/settings.routes.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { RouteObject } from "react-router-dom"; +import ProtectedRoute from "@/components/ProtectedRoute"; + +const Profile = React.lazy(() => import("@/pages/Profile")); +const EditProfile = React.lazy(() => import("@/pages/EditProfile")); + +export const settingsRoutes: RouteObject[] = [ + { + path: "/profile", + element: ( + + + + ), + }, + { + path: "/edit-profile", + element: ( + + + + ), + }, +]; diff --git a/supabase/migrations/20260617000000_consolidate_rls_policies.sql b/supabase/migrations/20260617000000_consolidate_rls_policies.sql index bfc73b7a..ba63581b 100644 --- a/supabase/migrations/20260617000000_consolidate_rls_policies.sql +++ b/supabase/migrations/20260617000000_consolidate_rls_policies.sql @@ -339,6 +339,7 @@ CREATE POLICY "Users can delete peer connections" ON public.peer_connections FOR DELETE USING (auth.uid() = sender_id OR auth.uid() = receiver_id); -- peer_submissions +ALTER TABLE public.peer_submissions ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view submissions" ON public.peer_submissions FOR SELECT USING (true); @@ -364,6 +365,7 @@ CREATE POLICY "Users can delete own submissions" ON public.peer_submissions FOR DELETE USING (user_id = auth.uid()); -- peer_reviews +ALTER TABLE public.peer_reviews ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view reviews" ON public.peer_reviews FOR SELECT USING (true);