From 3039e9e1ebb33666c53a21f185abbbcd471d6370 Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Wed, 7 Jan 2026 22:42:53 +0530 Subject: [PATCH 1/6] settings & home routes extended --- .../app/routes/auth-routes/v1/index.tsx | 4 +-- .../modules/app/routes/constants/routes.ts | 15 ++++++--- client/src/modules/home/modules/index.tsx | 3 ++ client/src/modules/home/routes/index.tsx | 18 +++++++++++ client/src/modules/settings/modules/index.tsx | 3 ++ .../get-settings.tsx => routes/index.tsx} | 31 +++++++++---------- client/src/modules/settings/v1/hooks/index.ts | 4 +-- 7 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 client/src/modules/home/modules/index.tsx create mode 100644 client/src/modules/home/routes/index.tsx create mode 100644 client/src/modules/settings/modules/index.tsx rename client/src/modules/settings/{v1/hooks/get-settings.tsx => routes/index.tsx} (55%) diff --git a/client/src/modules/app/routes/auth-routes/v1/index.tsx b/client/src/modules/app/routes/auth-routes/v1/index.tsx index 0b2bdf2f..914c30ec 100644 --- a/client/src/modules/app/routes/auth-routes/v1/index.tsx +++ b/client/src/modules/app/routes/auth-routes/v1/index.tsx @@ -11,8 +11,8 @@ import { export default function getRoutesV1() { const routes = [ }> diff --git a/client/src/modules/app/routes/constants/routes.ts b/client/src/modules/app/routes/constants/routes.ts index 01dc37ea..bda6ff06 100644 --- a/client/src/modules/app/routes/constants/routes.ts +++ b/client/src/modules/app/routes/constants/routes.ts @@ -1,10 +1,17 @@ +export enum ROUTES_PAGE_V1 { + HOME = 'home', + SETTINGS = 'settings', +} + export enum ROUTES_V1 { HOME = '/v1/home', SETTINGS = '/v1/settings', - SETTINGS_PROFILE = '/profile', } -export enum ROUTES_PAGE_V1 { - HOME = 'home', - SETTINGS = 'settings', +export enum ROUTES_HOME_V1 { + PROJECT = '/:project_id', +} + +export enum ROUTES_SETTINGS_V1 { + PROFILE = '/profile', } diff --git a/client/src/modules/home/modules/index.tsx b/client/src/modules/home/modules/index.tsx new file mode 100644 index 00000000..df93c4c6 --- /dev/null +++ b/client/src/modules/home/modules/index.tsx @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const ProjectLazyComponentV1 = lazy(() => import('./project/v1')); diff --git a/client/src/modules/home/routes/index.tsx b/client/src/modules/home/routes/index.tsx new file mode 100644 index 00000000..7c9e1bed --- /dev/null +++ b/client/src/modules/home/routes/index.tsx @@ -0,0 +1,18 @@ +import { Route } from 'react-router-dom'; +import { ProtectedRoute } from '../../app/routes/auth-routes/protected-route'; +import { ROUTES_HOME_V1 } from '../../app/routes/constants/routes'; +import { ProjectLazyComponentV1 } from '../modules'; + +export const homeRoutes = () => { + const routes: React.ReactNode[] = [ + + } + />, + ]; + + return { routes }; +}; diff --git a/client/src/modules/settings/modules/index.tsx b/client/src/modules/settings/modules/index.tsx new file mode 100644 index 00000000..05321729 --- /dev/null +++ b/client/src/modules/settings/modules/index.tsx @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const ProfileLazyComponentV1 = lazy(() => import('./profile/v1')); diff --git a/client/src/modules/settings/v1/hooks/get-settings.tsx b/client/src/modules/settings/routes/index.tsx similarity index 55% rename from client/src/modules/settings/v1/hooks/get-settings.tsx rename to client/src/modules/settings/routes/index.tsx index 6ccdb6a4..8141cfd0 100644 --- a/client/src/modules/settings/v1/hooks/get-settings.tsx +++ b/client/src/modules/settings/routes/index.tsx @@ -1,16 +1,15 @@ -/* eslint-disable react-refresh/only-export-components */ + import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined'; -import { ROUTES_V1 } from '../../../app/routes/constants/routes'; -import { SettingTabType } from '../typings'; +import { + ROUTES_V1, + ROUTES_SETTINGS_V1, +} from '../../app/routes/constants/routes'; +import { SettingTabType } from '../v1/typings'; import { Navigate, Route } from 'react-router-dom'; -import { ProtectedRoute } from '../../../app/routes/auth-routes/protected-route'; -import { lazy } from 'react'; +import { ProtectedRoute } from '../../app/routes/auth-routes/protected-route'; +import { ProfileLazyComponentV1 } from '../modules'; -export const ProfileLazyComponentV1 = lazy( - () => import('../../modules/profile/v1') -); - -export const getSettings = ({ +export const settingsRoutes = ({ isMobileOrTablet, }: { isMobileOrTablet: boolean; @@ -19,7 +18,7 @@ export const getSettings = ({ { id: 'profile', icon: , - path: ROUTES_V1.SETTINGS_PROFILE, + path: ROUTES_SETTINGS_V1.PROFILE, name: 'Your profile', description: 'Edit your personal details', }, @@ -27,8 +26,8 @@ export const getSettings = ({ const routes: React.ReactNode[] = [ } @@ -40,7 +39,7 @@ export const getSettings = ({ path="*" element={ } @@ -49,7 +48,7 @@ export const getSettings = ({ ]; return { - settings: settings, - routes: routes, + settings, + routes, }; }; diff --git a/client/src/modules/settings/v1/hooks/index.ts b/client/src/modules/settings/v1/hooks/index.ts index 459d583b..0b4f58c7 100644 --- a/client/src/modules/settings/v1/hooks/index.ts +++ b/client/src/modules/settings/v1/hooks/index.ts @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { getSettings } from './get-settings'; +import { settingsRoutes } from '../../routes'; import { ROUTES_V1 } from '../../../app/routes/constants/routes'; import { useDevice } from '../../../../shared/hooks/use-device'; @@ -13,7 +13,7 @@ const useSettingsV1 = () => { setSearchTerm(''); }; - const { settings, routes } = getSettings({ isMobileOrTablet }); + const { settings, routes } = settingsRoutes({ isMobileOrTablet }); const filteredSettings = useMemo(() => { if (!searchTerm) return settings; return settings.filter(setting => From 0295d90333bb4a3b5e75458722e6b8caf196fa72 Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Wed, 7 Jan 2026 23:04:16 +0530 Subject: [PATCH 2/6] optimize /me api calls --- client/src/shared/hooks/use-auth.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/client/src/shared/hooks/use-auth.ts b/client/src/shared/hooks/use-auth.ts index ac5037c5..58710747 100644 --- a/client/src/shared/hooks/use-auth.ts +++ b/client/src/shared/hooks/use-auth.ts @@ -1,5 +1,5 @@ -import { useSetAtom, useAtom } from 'jotai'; -import { useEffect, useState, useCallback } from 'react'; +import { useSetAtom, useAtom, useAtomValue } from 'jotai'; +import { useEffect, useState, useCallback, useRef } from 'react'; import { UserAtom } from '../../infra/states/user'; import { TokenAtom } from '../../infra/states/auth'; import { refreshToken } from '../../infra/rest/apis/auth'; @@ -13,8 +13,10 @@ import { TOKEN_CONFIG } from '../../config/env'; export const useAuth = () => { const [token, setToken] = useAtom(TokenAtom); + const user = useAtomValue(UserAtom); const setUser = useSetAtom(UserAtom); const [initialized, setInitialized] = useState(false); + const hasFetchedUserRef = useRef(false); // Initialize tokens and fetch user data from server on app start useEffect(() => { @@ -23,15 +25,20 @@ export const useAuth = () => { if (accessToken) { setToken(accessToken); - try { - const response = await getCurrentUser(); - if (response.status === 'success' && response.data) { - setUser(response.data); + // Only fetch user if we don't already have user data and haven't fetched yet + if (!user && !hasFetchedUserRef.current) { + hasFetchedUserRef.current = true; + try { + const response = await getCurrentUser(); + if (response.status === 'success' && response.data) { + setUser(response.data); + } + } catch (error) { + // If fetching user fails, token might be invalid + // Don't clear token here - let the interceptor handle it + console.error('Failed to fetch current user:', error); + hasFetchedUserRef.current = false; // Allow retry on next mount if needed } - } catch (error) { - // If fetching user fails, token might be invalid - // Don't clear token here - let the interceptor handle it - console.error('Failed to fetch current user:', error); } } From 85e3dc22864ff7071e19cce581ea9026f9b3ce42 Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Wed, 7 Jan 2026 23:18:45 +0530 Subject: [PATCH 3/6] home page states managed --- .../v1/components/banner-project-card.tsx | 75 +++++++------ .../home/v1/components/no-banner-project.tsx | 73 ++++++------ client/src/modules/home/v1/hooks/index.ts | 104 ++++++++++++------ client/src/modules/home/v1/index.tsx | 77 ++++++++----- client/src/modules/home/v1/states/index.ts | 11 +- 5 files changed, 201 insertions(+), 139 deletions(-) diff --git a/client/src/modules/home/v1/components/banner-project-card.tsx b/client/src/modules/home/v1/components/banner-project-card.tsx index 89fef768..5552e5cd 100644 --- a/client/src/modules/home/v1/components/banner-project-card.tsx +++ b/client/src/modules/home/v1/components/banner-project-card.tsx @@ -1,6 +1,6 @@ // TODO: Redesign this component to make it more visually appealing & interactive. import { Link } from 'react-router-dom'; -import { Avatar, Box, Chip, Stack, Typography, useTheme } from '@mui/material'; +import { Avatar, Box, Chip, Stack, useTheme } from '@mui/material'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import { useA2ZTheme } from '../../../../shared/hooks/use-theme'; import { getDay } from '../../../../shared/utils/date'; @@ -9,6 +9,8 @@ import { defaultLightThumbnail, } from '../../../editor/constants'; import { getAllProjectsResponse } from '../../../../infra/rest/apis/project/typing'; +import A2ZTypography from '../../../../shared/components/atoms/typography'; +import { ROUTES_V1 } from '../../../app/routes/constants/routes'; const BannerProjectCard = ({ project, @@ -31,7 +33,7 @@ const BannerProjectCard = ({ return ( - - {fullname} @{username} - - - {getDay(publishedAt)} - + + {/* Title */} - - {title} - + /> {/* Description */} - - {description} - + /> {/* Tags + Likes */} @@ -120,10 +127,10 @@ const BannerProjectCard = ({ direction="row" alignItems="center" spacing={1} - color="text.secondary" + sx={{ color: 'text.secondary' }} > - {total_likes} + diff --git a/client/src/modules/home/v1/components/no-banner-project.tsx b/client/src/modules/home/v1/components/no-banner-project.tsx index 14b10161..7dfa4fb1 100644 --- a/client/src/modules/home/v1/components/no-banner-project.tsx +++ b/client/src/modules/home/v1/components/no-banner-project.tsx @@ -1,7 +1,9 @@ import { Link } from 'react-router-dom'; -import { Avatar, Box, Typography, Stack, useTheme } from '@mui/material'; +import { Avatar, Box, Stack, useTheme } from '@mui/material'; import { getTrendingProjectsResponse } from '../../../../infra/rest/apis/project/typing'; import { getDay } from '../../../../shared/utils/date'; +import A2ZTypography from '../../../../shared/components/atoms/typography'; +import { ROUTES_V1 } from '../../../app/routes/constants/routes'; interface NoBannerProjectCardProps { project: getTrendingProjectsResponse; @@ -21,7 +23,7 @@ const NoBannerProjectCard = ({ project, index }: NoBannerProjectCardProps) => { return ( { }} > {/* Index Number */} - - {index < 10 ? `0${index + 1}` : index} - + /> {/* Main Content */} @@ -55,36 +58,42 @@ const NoBannerProjectCard = ({ project, index }: NoBannerProjectCardProps) => { alt={fullname} sx={{ width: 24, height: 24 }} /> - - {fullname} @{username} - - + - {getDay(publishedAt)} - + /> {/* Project Title */} - - {title} - + /> ); diff --git a/client/src/modules/home/v1/hooks/index.ts b/client/src/modules/home/v1/hooks/index.ts index 0446de65..2a4a08f2 100644 --- a/client/src/modules/home/v1/hooks/index.ts +++ b/client/src/modules/home/v1/hooks/index.ts @@ -1,62 +1,94 @@ +import { useCallback, useMemo, useState } from 'react'; import { useSetAtom } from 'jotai'; -import { HomePageProjectsAtom, HomePageTrendingProjectsAtom } from '../states'; +import { HomePageProjectsAtom } from '../states'; import { getAllProjects, getTrendingProjects, searchProjects, } from '../../../../infra/rest/apis/project'; +import { homeRoutes } from '../../routes'; +import { useLocation } from 'react-router-dom'; +import { ROUTES_V1 } from '../../../app/routes/constants/routes'; +import { getTrendingProjectsResponse } from '../../../../infra/rest/apis/project/typing'; -const useHome = () => { +const useHomeV1 = () => { + const { routes } = homeRoutes(); + const location = useLocation(); const setProjects = useSetAtom(HomePageProjectsAtom); - const setTrending = useSetAtom(HomePageTrendingProjectsAtom); - const fetchLatestProjects = async (page = 1) => { - const response = await getAllProjects(page); - if (response.data) { - setProjects(response.data); - } - }; + const [selectedCategory, setSelectedCategory] = useState(null); + const [trendingProjects, setTrendingProjects] = useState< + getTrendingProjectsResponse[] + >([]); + + const isHomePage = useMemo(() => { + const currentPath = location.pathname; + const homeBasePath = ROUTES_V1.HOME; + return currentPath === homeBasePath; + }, [location.pathname]); + + const fetchLatestProjects = useCallback( + async (page = 1) => { + if (!isHomePage) return; + const response = await getAllProjects(page); + if (response.data) { + setProjects(response.data); + } + }, + [setProjects] + ); - const fetchTrendingProjects = async () => { + const fetchTrendingProjects = useCallback(async () => { + if (!isHomePage) return; const response = await getTrendingProjects(); if (response.data) { - setTrending(response.data); + setTrendingProjects(response.data); } - }; + }, [setTrendingProjects]); - const fetchProjectsByCategory = async ({ - tag, - query, - user_id, - page = 1, - limit = 10, - rmv_project_by_id, - }: { - tag?: string; - query?: string; - user_id?: string; - page?: number; - limit?: number; - rmv_project_by_id?: string; - }) => { - const response = await searchProjects({ + const fetchProjectsByCategory = useCallback( + async ({ tag, query, user_id, - page, - limit, + page = 1, + limit = 10, rmv_project_by_id, - }); - if (response.data) { - setProjects(response.data); - } - }; + }: { + tag?: string; + query?: string; + user_id?: string; + page?: number; + limit?: number; + rmv_project_by_id?: string; + }) => { + if (!isHomePage) return; + const response = await searchProjects({ + tag, + query, + user_id, + page, + limit, + rmv_project_by_id, + }); + if (response.data) { + setProjects(response.data); + } + }, + [setProjects] + ); return { + routes, + isHomePage, + selectedCategory, + setSelectedCategory, + trendingProjects, + setTrendingProjects, fetchLatestProjects, fetchTrendingProjects, fetchProjectsByCategory, }; }; -export default useHome; +export default useHomeV1; diff --git a/client/src/modules/home/v1/index.tsx b/client/src/modules/home/v1/index.tsx index 1fca85c8..5142e43f 100644 --- a/client/src/modules/home/v1/index.tsx +++ b/client/src/modules/home/v1/index.tsx @@ -1,16 +1,13 @@ import { Box, Stack } from '@mui/material'; +import { Routes } from 'react-router-dom'; import A2ZTypography from '../../../shared/components/atoms/typography'; import TrendingUpIcon from '@mui/icons-material/TrendingUp'; import { categories } from './constants'; import { CategoryButton } from './components/category-button'; import InPageNavigation from '../../../shared/components/molecules/page-navigation'; import NoBannerProjectCard from './components/no-banner-project'; -import { useAtom, useAtomValue } from 'jotai'; -import { - HomePageProjectsAtom, - HomePageStateAtom, - HomePageTrendingProjectsAtom, -} from './states'; +import { useAtomValue } from 'jotai'; +import { HomePageProjectsAtom } from './states'; import BannerProjectCard from './components/banner-project-card'; import NoDataMessageBox from '../../../shared/components/atoms/no-data-msg'; import { @@ -18,34 +15,55 @@ import { NoBannerSkeleton, } from '../../../shared/components/atoms/skeleton'; import { useEffect } from 'react'; -import useHome from './hooks'; +import useHomeV1 from './hooks'; import { Virtuoso } from 'react-virtuoso'; const Home = () => { - const [pageState, setPageState] = useAtom(HomePageStateAtom); const projects = useAtomValue(HomePageProjectsAtom); - const trending = useAtomValue(HomePageTrendingProjectsAtom); const { + routes, + isHomePage, + selectedCategory, + setSelectedCategory, + trendingProjects, fetchLatestProjects, fetchTrendingProjects, fetchProjectsByCategory, - } = useHome(); + } = useHomeV1(); useEffect(() => { - if (pageState === 'home') { - fetchLatestProjects(); - } else if (pageState !== 'trending') { - fetchProjectsByCategory({ tag: pageState }); + // Only fetch projects if not on project page + if (isHomePage) { + if (!selectedCategory) { + fetchLatestProjects(); + } else if (selectedCategory !== 'trending') { + fetchProjectsByCategory({ tag: selectedCategory }); + } + fetchTrendingProjects(); } - fetchTrendingProjects(); }, [ - pageState, + selectedCategory, fetchLatestProjects, fetchProjectsByCategory, fetchTrendingProjects, + isHomePage, ]); + if (!isHomePage) { + return ( + + {routes} + + ); + } + return ( { {/* Latest projects */} {projects.length ? ( @@ -73,10 +91,13 @@ const Home = () => { overscan={200} endReached={() => { const nextPage = Math.floor(projects.length / 10) + 1; // Assuming page size of 10 - if (pageState === 'home') { + if (!selectedCategory) { fetchLatestProjects(nextPage); - } else if (pageState !== 'trending') { - fetchProjectsByCategory({ page: nextPage, tag: pageState }); + } else if (selectedCategory !== 'trending') { + fetchProjectsByCategory({ + page: nextPage, + tag: selectedCategory, + }); } }} components={{ @@ -89,10 +110,10 @@ const Home = () => { ) : ( )} - {trending && trending.length === 0 ? ( // FIX ME + {trendingProjects && trendingProjects.length === 0 ? ( // FIX ME - ) : trending && trending.length ? ( - trending.map((project, i) => { + ) : trendingProjects && trendingProjects.length ? ( + trendingProjects.map((project, i) => { return ( ); @@ -128,7 +149,9 @@ const Home = () => { { - setPageState(pageState === category ? 'home' : category); + setSelectedCategory( + selectedCategory === category ? null : category + ); }} > {category} @@ -157,10 +180,10 @@ const Home = () => { - {trending && trending.length === 0 ? ( // FIX ME + {trendingProjects && trendingProjects.length === 0 ? ( // FIX ME - ) : trending && trending.length ? ( - trending.map((project, i) => { + ) : trendingProjects && trendingProjects.length ? ( + trendingProjects.map((project, i) => { return ( ); diff --git a/client/src/modules/home/v1/states/index.ts b/client/src/modules/home/v1/states/index.ts index 847c6d2e..554183cf 100644 --- a/client/src/modules/home/v1/states/index.ts +++ b/client/src/modules/home/v1/states/index.ts @@ -1,13 +1,4 @@ import { atom } from 'jotai'; -import { - getAllProjectsResponse, - getTrendingProjectsResponse, -} from '../../../../infra/rest/apis/project/typing'; - -export const HomePageStateAtom = atom('home'); +import { getAllProjectsResponse } from '../../../../infra/rest/apis/project/typing'; export const HomePageProjectsAtom = atom([]); - -export const HomePageTrendingProjectsAtom = atom( - [] -); From 9a48dc3260ee90879859790c8b20d0e00a7342a5 Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Thu, 8 Jan 2026 00:28:18 +0530 Subject: [PATCH 4/6] setup homepage navbar --- client/src/modules/home/v1/index.tsx | 233 +++++++------- .../components/atoms/typography/index.tsx | 8 +- .../components/molecules/searchbar/index.tsx | 299 ++++++++++-------- .../navbar/components/render-menu.tsx | 96 ------ .../navbar/components/render-mobile-menu.tsx | 91 +----- .../organisms/navbar/hooks/index.ts | 92 ++---- .../components/organisms/navbar/index.tsx | 250 +++++---------- 7 files changed, 413 insertions(+), 656 deletions(-) delete mode 100644 client/src/shared/components/organisms/navbar/components/render-menu.tsx diff --git a/client/src/modules/home/v1/index.tsx b/client/src/modules/home/v1/index.tsx index 5142e43f..d7a99de9 100644 --- a/client/src/modules/home/v1/index.tsx +++ b/client/src/modules/home/v1/index.tsx @@ -17,6 +17,7 @@ import { import { useEffect } from 'react'; import useHomeV1 from './hooks'; import { Virtuoso } from 'react-virtuoso'; +import Navbar from '../../../shared/components/organisms/navbar'; const Home = () => { const projects = useAtomValue(HomePageProjectsAtom); @@ -65,121 +66,58 @@ const Home = () => { } return ( - - {/* Latest projects */} - - - {projects.length ? ( - ( - - )} - overscan={200} - endReached={() => { - const nextPage = Math.floor(projects.length / 10) + 1; // Assuming page size of 10 - if (!selectedCategory) { - fetchLatestProjects(nextPage); - } else if (selectedCategory !== 'trending') { - fetchProjectsByCategory({ - page: nextPage, - tag: selectedCategory, - }); - } - }} - components={{ - Footer: () => - !projects || projects.length === 0 ? ( - - ) : null, // FIX ME - }} - /> - ) : ( - - )} - {trendingProjects && trendingProjects.length === 0 ? ( // FIX ME - - ) : trendingProjects && trendingProjects.length ? ( - trendingProjects.map((project, i) => { - return ( - - ); - }) - ) : ( - - )} - - + <> + - {/* filters and trending projects */} `1px solid ${theme.palette.divider}`, - pl: 4, - pt: 1, - display: { xs: 'none', md: 'block' }, + minHeight: 'calc(100vh - 150px)', + display: 'flex', + justifyContent: 'center', + gap: { xs: 0, md: 5 }, + p: 3, }} > - - - - - - {categories.map((category, i) => { - return ( - { - setSelectedCategory( - selectedCategory === category ? null : category - ); - }} - > - {category} - - ); - })} - - - - + - - - - - + {projects.length ? ( + ( + + )} + overscan={200} + endReached={() => { + const nextPage = Math.floor(projects.length / 10) + 1; // Assuming page size of 10 + if (!selectedCategory) { + fetchLatestProjects(nextPage); + } else if (selectedCategory !== 'trending') { + fetchProjectsByCategory({ + page: nextPage, + tag: selectedCategory, + }); + } + }} + components={{ + Footer: () => + !projects || projects.length === 0 ? ( + + ) : null, // FIX ME + }} + /> + ) : ( + + )} {trendingProjects && trendingProjects.length === 0 ? ( // FIX ME ) : trendingProjects && trendingProjects.length ? ( @@ -191,10 +129,81 @@ const Home = () => { ) : ( )} - - + + + + {/* filters and trending projects */} + `1px solid ${theme.palette.divider}`, + pl: 4, + pt: 1, + display: { xs: 'none', md: 'block' }, + }} + > + + + + + + {categories.map((category, i) => { + return ( + { + setSelectedCategory( + selectedCategory === category ? null : category + ); + }} + > + {category} + + ); + })} + + + + + + + + + + {trendingProjects && trendingProjects.length === 0 ? ( // FIX ME + + ) : trendingProjects && trendingProjects.length ? ( + trendingProjects.map((project, i) => { + return ( + + ); + }) + ) : ( + + )} + + + - + ); }; diff --git a/client/src/shared/components/atoms/typography/index.tsx b/client/src/shared/components/atoms/typography/index.tsx index 9f1cb579..bd6f47b3 100644 --- a/client/src/shared/components/atoms/typography/index.tsx +++ b/client/src/shared/components/atoms/typography/index.tsx @@ -24,6 +24,12 @@ const A2ZTypography = ({ noWrap?: boolean; props?: Record; }) => { + // If dangerouslySetInnerHTML is provided, don't render text as children + const hasDangerousHTML = + props && + 'dangerouslySetInnerHTML' in props && + props.dangerouslySetInnerHTML; + return ( - {text} + {hasDangerousHTML ? null : text} ); }; diff --git a/client/src/shared/components/molecules/searchbar/index.tsx b/client/src/shared/components/molecules/searchbar/index.tsx index 390a4bfb..f0c38830 100644 --- a/client/src/shared/components/molecules/searchbar/index.tsx +++ b/client/src/shared/components/molecules/searchbar/index.tsx @@ -1,4 +1,11 @@ -import React, { useState } from 'react'; +import { + Ref, + KeyboardEvent, + ChangeEvent, + useState, + forwardRef, + useRef, +} from 'react'; import { Box, IconButton, @@ -22,152 +29,184 @@ interface SearchbarProps { variant?: 'fullWidth' | 'iconButton' | 'auto'; sx?: SxProps; autoFocus?: boolean; + onKeyDown?: (event: KeyboardEvent) => void; + inputRef?: Ref; + showClearButton?: boolean; } -const Searchbar: React.FC = ({ - customCSS = {}, - placeholder = 'Search settings...', - showLoading = false, - onSearch, - searchTerm, - handleOnClearClick, - variant = 'auto', - sx = {}, - autoFocus = true, -}) => { - const theme = useTheme(); - const { isMobile } = useDevice(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); +const Searchbar = forwardRef( + ( + { + customCSS = {}, + placeholder = 'Search settings...', + showLoading = false, + onSearch, + searchTerm, + handleOnClearClick, + variant = 'auto', + sx = {}, + autoFocus = true, + onKeyDown, + inputRef, + showClearButton = true, + }, + ref + ) => { + const theme = useTheme(); + const { isMobile } = useDevice(); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const internalInputRef = useRef(null); - const handleInputChange = (value: string) => { - onSearch(value); - }; + const handleInputChange = (value: string) => { + onSearch(value); + }; - const shouldShowIconButton = - variant === 'iconButton' || (variant === 'auto' && isMobile); + const shouldShowIconButton = + variant === 'iconButton' || (variant === 'auto' && isMobile); - const handleIconButtonClick = () => { - setIsDrawerOpen(true); - }; + const handleIconButtonClick = () => { + setIsDrawerOpen(true); + }; - const handleDrawerClose = () => { - setIsDrawerOpen(false); - }; + const handleDrawerClose = () => { + setIsDrawerOpen(false); + }; - const searchbarContent = ( - - + const handleClearClick = () => { + handleOnClearClick(); + // Focus the input after clearing + setTimeout(() => { + const refToUse = inputRef || ref || internalInputRef; + if ( + refToUse && + typeof refToUse === 'object' && + 'current' in refToUse && + refToUse.current + ) { + refToUse.current.focus(); } + }, 0); + }; + + const searchbarContent = ( + ) => - handleInputChange(e.target.value), - autoFocus: autoFocus && !shouldShowIconButton, - }, - }} - /> - {showLoading && ( - - - - )} - {searchTerm && ( - - - - )} - - ); - - if (shouldShowIconButton) { - return ( - <> - + + } sx={{ - color: 'text.primary', - '&:hover': { - bgcolor: 'action.hover', + flex: 1, + '.MuiFormControl-root': { margin: '0' }, + '.MuiInputBase-root': { + bgcolor: 'background.paper', + border: `1px solid ${theme.palette.divider}`, + borderRadius: 1, + '&::before': { border: 'none' }, + '&::after': { border: 'none' }, + '&:hover:not(.Mui-disabled)': { + borderColor: 'primary.main', + }, + '&.Mui-focused': { + borderColor: 'primary.main', + }, + 'input::placeholder, textArea::placeholder': { + color: 'text.secondary', + }, + input: { + color: 'text.primary', + py: 1, + }, + ...customCSS, }, }} - aria-label="Open search" - > - - - ) => + handleInputChange(e.target.value), + onKeyDown: onKeyDown, + autoFocus: autoFocus && !shouldShowIconButton, }, }} - > - {searchbarContent} - - + /> + {showLoading && ( + + + + )} + {showClearButton && searchTerm && ( + + + + )} + ); + + if (shouldShowIconButton) { + return ( + <> + + + + + {searchbarContent} + + + ); + } + + return searchbarContent; } +); - return searchbarContent; -}; +Searchbar.displayName = 'Searchbar'; export default Searchbar; diff --git a/client/src/shared/components/organisms/navbar/components/render-menu.tsx b/client/src/shared/components/organisms/navbar/components/render-menu.tsx deleted file mode 100644 index 648e8f69..00000000 --- a/client/src/shared/components/organisms/navbar/components/render-menu.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { FC } from 'react'; -import { Menu, MenuItem, Divider } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { useAtomValue } from 'jotai'; -import { UserAtom } from '../../../../../infra/states/user'; -import { useAuth } from '../../../../../shared/hooks/use-auth'; -import { logout as logoutApi } from '../../../../../infra/rest/apis/auth'; -import { menuId } from '../constants'; -import PersonIcon from '@mui/icons-material/Person'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import SettingsIcon from '@mui/icons-material/Settings'; -import LogoutIcon from '@mui/icons-material/Logout'; -import A2ZTypography from '../../../atoms/typography'; - -interface RenderMenuProps { - anchorEl: HTMLElement | null; - isMenuOpen: boolean; - handleMenuClose: () => void; -} - -const RenderMenu: FC = ({ - anchorEl, - isMenuOpen, - handleMenuClose, -}) => { - const navigate = useNavigate(); - const user = useAtomValue(UserAtom); - const { logout } = useAuth(); - - const handleProfileClick = () => { - if (user?.personal_info?.username) { - navigate(`/user/${user.personal_info.username}`); - } - handleMenuClose(); - }; - - const handleDashboardClick = () => { - navigate('/dashboard/projects'); - handleMenuClose(); - }; - - const handleSettingsClick = () => { - navigate('/settings/edit-profile'); - handleMenuClose(); - }; - - const handleLogout = async () => { - try { - await logoutApi(); - } catch (error) { - console.error('Logout error:', error); - } finally { - logout(); - navigate('/'); - handleMenuClose(); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - ); -}; - -export default RenderMenu; diff --git a/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx b/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx index c39c905e..fa7ff608 100644 --- a/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx +++ b/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx @@ -1,39 +1,25 @@ import { Menu, MenuItem, Badge } from '@mui/material'; import { FC } from 'react'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; import CreateIcon from '@mui/icons-material/Create'; -import LoginIcon from '@mui/icons-material/Login'; -import { menuId, mobileMenuId } from '../constants'; +import { mobileMenuId } from '../constants'; import A2ZIconButton from '../../../atoms/icon-button'; import A2ZTypography from '../../../atoms/typography'; -import AccountCircle from '@mui/icons-material/AccountCircle'; import MailIcon from '@mui/icons-material/Mail'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import { THEME } from '../../../../states/theme'; -import { useA2ZTheme } from '../../../../hooks/use-theme'; import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../../../hooks/use-auth'; interface RenderMobileMenuProps { mobileMoreAnchorEl: HTMLElement | null; isMobileMenuOpen: boolean; - handleProfileMenuOpen: (event: React.MouseEvent) => void; handleMobileMenuClose: () => void; setShowSubscribeModal: (show: boolean) => void; - notificationCount: number; } const RenderMobileMenu: FC = ({ mobileMoreAnchorEl, isMobileMenuOpen, - handleProfileMenuOpen, handleMobileMenuClose, setShowSubscribeModal, - notificationCount, }) => { - const { isAuthenticated } = useAuth(); - const { theme, setTheme } = useA2ZTheme(); const navigate = useNavigate(); return ( = ({ > { - setTheme(theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT); + navigate('/editor'); handleMobileMenuClose(); }} > - {theme === THEME.DARK ? : } + - + { - navigate('/editor'); + setShowSubscribeModal(true); handleMobileMenuClose(); }} > - + - + - - {isAuthenticated() - ? [ - { - navigate('/dashboard/notifications'); - handleMobileMenuClose(); - }} - > - - - - - - - , - - - - - - , - ] - : [ - { - setShowSubscribeModal(true); - handleMobileMenuClose(); - }} - > - - - - - - - , - { - navigate('/login'); - handleMobileMenuClose(); - }} - > - - - - - - - , - ]} ); }; diff --git a/client/src/shared/components/organisms/navbar/hooks/index.ts b/client/src/shared/components/organisms/navbar/hooks/index.ts index 4ae4ea11..819c1ffc 100644 --- a/client/src/shared/components/organisms/navbar/hooks/index.ts +++ b/client/src/shared/components/organisms/navbar/hooks/index.ts @@ -1,53 +1,43 @@ -import { useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDevice } from '../../../../hooks/use-device'; import { useNotifications } from '../../../../hooks/use-notification'; import { emailRegex } from '../../../../utils/regex'; import { subscribeUser } from '../../../../../infra/rest/apis/subscriber'; -import { allNotificationCounts } from '../../../../../infra/rest/apis/notification'; -import { NOTIFICATION_FILTER_TYPE } from '../../../../../infra/rest/typings'; export const useNavbar = () => { const navigate = useNavigate(); const { isDesktop } = useDevice(); + const searchInputRef = useRef(null); - const [anchorEl, setAnchorEl] = useState(null); const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = useState(null); - const [searchBoxVisibility, setSearchBoxVisibility] = - useState(false); - const [notificationCount, setNotificationCount] = useState(0); + const [searchTerm, setSearchTerm] = useState(''); - const searchRef = useRef(null); - const isMenuOpen = Boolean(anchorEl); const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); - const handleProfileMenuOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - const handleMobileMenuClose = () => { setMobileMoreAnchorEl(null); }; - const handleMenuClose = () => { - setAnchorEl(null); - handleMobileMenuClose(); - }; - const handleMobileMenuOpen = (event: React.MouseEvent) => { setMobileMoreAnchorEl(event.currentTarget); }; - const handleSearch = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.keyCode === 13) { - const query = event.currentTarget.value; - if (!query.trim().length) { - navigate('/'); - return; - } - navigate(`/search/${encodeURIComponent(query)}`); + const handleSearchChange = (value: string) => { + setSearchTerm(value); + }; + + const handleSearchSubmit = () => { + if (!searchTerm.trim().length) { + navigate('/'); + return; } + navigate(`/search/${encodeURIComponent(searchTerm)}`); + }; + + const handleClearSearch = () => { + setSearchTerm(''); }; const triggerSearchByKeyboard = (e: KeyboardEvent) => { @@ -55,54 +45,34 @@ export const useNavbar = () => { e.preventDefault(); e.stopPropagation(); - if (isDesktop) { + if (isDesktop && searchInputRef.current) { setTimeout(() => { - if (searchRef.current) { - searchRef.current.focus(); - searchRef.current.select(); + if (searchInputRef.current) { + searchInputRef.current.focus(); + searchInputRef.current.select(); } }, 10); - } else { - setSearchBoxVisibility(true); - setTimeout(() => { - if (searchRef.current) { - searchRef.current.focus(); - } - }, 100); } } }; - const fetchNotificationCount = async () => { - try { - const response = await allNotificationCounts({ - filter: NOTIFICATION_FILTER_TYPE.ALL, - }); - if (response.status === 'success') { - setNotificationCount(response?.data?.totalDocs || 0); - } - } catch (error) { - console.error('Error fetching notification count:', error); - } - }; + useEffect(() => { + document.addEventListener('keydown', triggerSearchByKeyboard, true); + return () => { + document.removeEventListener('keydown', triggerSearchByKeyboard, true); + }; + }, [isDesktop]); return { - anchorEl, mobileMoreAnchorEl, - isMenuOpen, isMobileMenuOpen, - handleProfileMenuOpen, handleMobileMenuClose, - handleMenuClose, handleMobileMenuOpen, - handleSearch, - searchRef, - searchBoxVisibility, - setSearchBoxVisibility, - triggerSearchByKeyboard, - notificationCount, - setNotificationCount, - fetchNotificationCount, + searchTerm, + handleSearchChange, + handleSearchSubmit, + handleClearSearch, + searchInputRef, }; }; diff --git a/client/src/shared/components/organisms/navbar/index.tsx b/client/src/shared/components/organisms/navbar/index.tsx index 349e8040..ff63ab6c 100644 --- a/client/src/shared/components/organisms/navbar/index.tsx +++ b/client/src/shared/components/organisms/navbar/index.tsx @@ -1,50 +1,27 @@ -import { AppBar, Box, Toolbar, Badge } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import AccountCircle from '@mui/icons-material/AccountCircle'; +import { Box, Badge, useTheme } from '@mui/material'; import MailIcon from '@mui/icons-material/Mail'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LoginIcon from '@mui/icons-material/Login'; import CreateIcon from '@mui/icons-material/Create'; import MoreIcon from '@mui/icons-material/MoreVert'; import A2ZIconButton from '../../atoms/icon-button'; -import A2ZTypography from '../../atoms/typography'; import { useNavbar, useSubscribe } from './hooks'; -import { menuId, mobileMenuId } from './constants'; +import { mobileMenuId } from './constants'; import RenderMobileMenu from './components/render-mobile-menu'; -import RenderMenu from './components/render-menu'; import SubscribeModal from './components/subscribe'; -import InputBox from '../../atoms/input-box'; -import { useEffect } from 'react'; -import { useDevice } from '../../../hooks/use-device'; -import { THEME } from '../../../states/theme'; -import { useA2ZTheme } from '../../../hooks/use-theme'; -import { Outlet, useNavigate } from 'react-router-dom'; -import { useAuth } from '../../../hooks/use-auth'; -import { useThrottledFetch } from '../../../hooks/use-throttle-fetch'; +import Searchbar from '../../molecules/searchbar'; +import { HEADER_HEIGHT } from '../header/constants'; const Navbar = () => { - const { isAuthenticated } = useAuth(); - const { isDesktop } = useDevice(); - const { theme, setTheme } = useA2ZTheme(); - const navigate = useNavigate(); + const theme = useTheme(); const { - anchorEl, mobileMoreAnchorEl, - isMenuOpen, isMobileMenuOpen, - handleProfileMenuOpen, handleMobileMenuOpen, - handleMenuClose, handleMobileMenuClose, - handleSearch, - searchRef, - searchBoxVisibility, - setSearchBoxVisibility, - triggerSearchByKeyboard, - notificationCount, - fetchNotificationCount, + searchTerm, + handleSearchChange, + handleSearchSubmit, + handleClearSearch, + searchInputRef, } = useNavbar(); const { @@ -54,153 +31,80 @@ const Navbar = () => { handleSubscribe, } = useSubscribe(); - useEffect(() => { - document.addEventListener('keydown', triggerSearchByKeyboard, true); - return () => { - document.removeEventListener('keydown', triggerSearchByKeyboard, true); - }; - }, [isDesktop, triggerSearchByKeyboard]); - - useThrottledFetch({ - isAuthenticated: isAuthenticated(), - fetch: fetchNotificationCount, - storageKey: 'a2z_notifications_last_fetch', - }); - return ( - <> - - - - - Logo - - - - } - sx={{ - padding: { xs: 0, sm: '0 1.25rem' }, - }} - slotProps={{ - htmlInput: { - onFocus: () => setSearchBoxVisibility(true), - onBlur: () => setSearchBoxVisibility(false), - onKeyDown: handleSearch, - sx: { - padding: '10px 0', - }, - }, - }} - /> - - - - - - - setTheme(theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT) - } - > - {theme === THEME.DARK ? : } - - - - - - - - + + + { + if (e.key === 'Enter' || e.keyCode === 13) { + handleSearchSubmit(); + } + }} + /> - {isAuthenticated() ? ( - <> - - - - - - - - - - ) : ( - <> - - setShowSubscribeModal(true)}> - - - - - navigate('/login')}> - - - - - )} - + - - - - - - - + + + + + + - - + + setShowSubscribeModal(true)}> + + + + - + + + + + - - + + + + ); }; From f88767d318d7d2d43086c8f9feb8903dff4415bb Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Thu, 8 Jan 2026 00:34:09 +0530 Subject: [PATCH 5/6] migrate project v1 route --- .../project/v1/components/project-content.tsx | 232 +++++++++++++++++ .../v1}/components/project-interaction.tsx | 81 ++++-- .../modules/project/v1}/hooks/index.ts | 34 ++- .../v1}/hooks/use-project-interaction.ts | 15 +- .../modules/home/modules/project/v1/index.tsx | 241 ++++++++++++++++++ .../modules/project/v1}/states/index.ts | 2 +- .../components/publish-projects.tsx | 3 +- .../components/notificationCard.tsx | 3 +- client/src/modules/profile/hooks/index.ts | 4 +- client/src/modules/profile/index.tsx | 4 +- .../project/components/project-content.tsx | 162 ------------ client/src/modules/project/index.tsx | 211 --------------- client/src/modules/search/index.tsx | 4 +- .../components/comment-card.tsx | 2 +- .../components/comment-field.tsx | 2 +- .../organisms/comments-wrapper/hooks/index.ts | 85 ++++-- .../organisms/comments-wrapper/index.tsx | 2 +- 17 files changed, 631 insertions(+), 456 deletions(-) create mode 100644 client/src/modules/home/modules/project/v1/components/project-content.tsx rename client/src/modules/{project => home/modules/project/v1}/components/project-interaction.tsx (61%) rename client/src/modules/{project => home/modules/project/v1}/hooks/index.ts (53%) rename client/src/modules/{project => home/modules/project/v1}/hooks/use-project-interaction.ts (69%) create mode 100644 client/src/modules/home/modules/project/v1/index.tsx rename client/src/modules/{project => home/modules/project/v1}/states/index.ts (65%) delete mode 100644 client/src/modules/project/components/project-content.tsx delete mode 100644 client/src/modules/project/index.tsx diff --git a/client/src/modules/home/modules/project/v1/components/project-content.tsx b/client/src/modules/home/modules/project/v1/components/project-content.tsx new file mode 100644 index 00000000..8713bba5 --- /dev/null +++ b/client/src/modules/home/modules/project/v1/components/project-content.tsx @@ -0,0 +1,232 @@ +import { useState } from 'react'; +import { Box, Button, useTheme } from '@mui/material'; +import { OutputBlockData } from '@editorjs/editorjs'; +import A2ZTypography from '../../../../../../shared/components/atoms/typography'; + +const ParagraphBlock = ({ text }: { text: string }) => { + return ( + + ); +}; + +const HeaderBlock = ({ level, text }: { level: number; text: string }) => { + const variant = level === 3 ? 'h5' : 'h4'; + const component = level === 3 ? 'h3' : 'h2'; + return ( + + ); +}; + +const ImageBlock = ({ url, caption }: { url: string; caption: string }) => { + return ( + + {caption} + {caption && ( + + )} + + ); +}; + +const QuoteBlock = ({ text, caption }: { text: string; caption: string }) => { + const theme = useTheme(); + return ( + + + {caption && ( + + )} + + ); +}; + +const ListBlock = ({ style, items }: { style: string; items: string[] }) => { + const Tag = style === 'ordered' ? 'ol' : 'ul'; + const styleType = style === 'ordered' ? 'decimal' : 'disc'; + return ( + + {items.map((item, i) => ( +
  • + +
  • + ))} +
    + ); +}; + +const CodeBlock = ({ code, language }: { code: string; language: string }) => { + const theme = useTheme(); + const codeText = code || ''; + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(codeText); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + setCopied(false); + } + }; + + return ( + + {language && ( + + )} + + + {codeText} + + + ); +}; + +const ProjectContent = ({ block }: { block: OutputBlockData }) => { + const { type, data } = block || {}; + + switch (type) { + case 'paragraph': + return ; + case 'header': + return ; + case 'image': + return ( + + ); + case 'quote': + return ( + + ); + case 'list': + return ; + case 'code': + return ( + + ); + default: + return null; + } +}; + +export default ProjectContent; diff --git a/client/src/modules/project/components/project-interaction.tsx b/client/src/modules/home/modules/project/v1/components/project-interaction.tsx similarity index 61% rename from client/src/modules/project/components/project-interaction.tsx rename to client/src/modules/home/modules/project/v1/components/project-interaction.tsx index 8cc109c5..89f2ee30 100644 --- a/client/src/modules/project/components/project-interaction.tsx +++ b/client/src/modules/home/modules/project/v1/components/project-interaction.tsx @@ -1,21 +1,24 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useEffect } from 'react'; import { Link } from 'react-router-dom'; -import { IconButton, Box, Divider, Typography, Tooltip } from '@mui/material'; +import { IconButton, Box, Divider, Tooltip } from '@mui/material'; import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; import EditIcon from '@mui/icons-material/Edit'; import XIcon from '@mui/icons-material/X'; -import { UserAtom } from '../../../infra/states/user'; +import { UserAtom } from '../../../../../../infra/states/user'; import { LikedByUserAtom, SelectedProjectAtom } from '../states'; -import { useAuth } from '../../../shared/hooks/use-auth'; -import { likeStatus } from '../../../infra/rest/apis/like'; +import { likeStatus } from '../../../../../../infra/rest/apis/like'; import useProjectInteraction from '../hooks/use-project-interaction'; -import { CommentsWrapperAtom } from '../../../shared/components/organisms/comments-wrapper/states'; +import { CommentsWrapperAtom } from '../../../../../../shared/components/organisms/comments-wrapper/states'; +import A2ZTypography from '../../../../../../shared/components/atoms/typography'; + +// Module-level refs to prevent duplicate API calls across component instances +const fetchedProjectIdRef: { current: string | null } = { current: null }; +const fetchingLikeStatusRef: { current: boolean } = { current: false }; const ProjectInteraction = () => { - const { isAuthenticated } = useAuth(); const user = useAtomValue(UserAtom); const project = useAtomValue(SelectedProjectAtom); const [islikedByUser, setLikedByUser] = useAtom(LikedByUserAtom); @@ -24,28 +27,51 @@ const ProjectInteraction = () => { useEffect(() => { const fetchLikeStatus = async () => { - if (!isAuthenticated() || !project?._id) return; + if (!project?._id) { + // Reset refs if project is cleared + fetchedProjectIdRef.current = null; + fetchingLikeStatusRef.current = false; + return; + } + + // Prevent fetching if we've already fetched for this project or are currently fetching + if ( + fetchedProjectIdRef.current === project._id || + fetchingLikeStatusRef.current + ) { + return; + } + + fetchedProjectIdRef.current = project._id; + fetchingLikeStatusRef.current = true; + try { const response = await likeStatus(project._id); - if (response.is_liked) { - setLikedByUser(Boolean(response.is_liked)); - } + setLikedByUser(Boolean(response.is_liked)); } catch (error) { console.error('Error fetching like status:', error); + // Reset on error to allow retry + fetchedProjectIdRef.current = null; + } finally { + fetchingLikeStatusRef.current = false; } }; fetchLikeStatus(); - }, [project?._id, setLikedByUser, isAuthenticated]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project?._id]); return ( <> {/* Left side — Likes & Comments */} @@ -69,29 +95,33 @@ const ProjectInteraction = () => { {islikedByUser ? : } - - {project?.activity.total_likes} - + {/* Comment Button */} setCommentsWrapper(prev => !prev)} sx={{ - bgcolor: theme => theme.palette.action.hover, + bgcolor: 'action.hover', color: 'text.secondary', transition: 'background-color 0.2s, color 0.2s', '&:hover': { - bgcolor: theme => theme.palette.action.selected, + bgcolor: 'action.selected', }, }} > - - {project?.activity.total_comments} - + {/* Right side — Edit & Share */} @@ -114,9 +144,10 @@ const ProjectInteraction = () => { { const setSelectedProject = useSetAtom(SelectedProjectAtom); - const { fetchProjectsByCategory } = useHome(); + const { fetchProjectsByCategory } = useHomeV1(); const { fetchComments } = useCommentsWrapper(); const [loading, setLoading] = useState(true); + const currentProjectIdRef = useRef(null); const fetchProject = useCallback( async (project_id: string) => { - if (!project_id || project_id.trim() === '') return; + if (!project_id || project_id.trim() === '') { + setLoading(false); + return; + } + + // Prevent fetching the same project multiple times + if (currentProjectIdRef.current === project_id) { + return; + } + + currentProjectIdRef.current = project_id; + setLoading(true); try { const response = await getProjectById(project_id); if (response.data) { - // Fetch comments for the project - await fetchComments({ project_id: response.data._id || '' }); setSelectedProject(response.data); + // Fetch comments for the project (reset comments for new project) + await fetchComments({ + project_id: response.data._id || '', + reset: true, + }); + // Fetch similar projects if (response.data.tags && response.data.tags.length > 0) { await fetchProjectsByCategory({ @@ -37,6 +53,7 @@ const useProject = () => { } catch (error) { console.error('Error fetching project:', error); setLoading(false); + currentProjectIdRef.current = null; } }, [setSelectedProject, fetchProjectsByCategory, fetchComments] @@ -44,7 +61,6 @@ const useProject = () => { return { loading, - fetchComments, fetchProject, }; }; diff --git a/client/src/modules/project/hooks/use-project-interaction.ts b/client/src/modules/home/modules/project/v1/hooks/use-project-interaction.ts similarity index 69% rename from client/src/modules/project/hooks/use-project-interaction.ts rename to client/src/modules/home/modules/project/v1/hooks/use-project-interaction.ts index d7e5a776..244cd77e 100644 --- a/client/src/modules/project/hooks/use-project-interaction.ts +++ b/client/src/modules/home/modules/project/v1/hooks/use-project-interaction.ts @@ -1,21 +1,16 @@ import { useAtom } from 'jotai'; -import { useAuth } from '../../../shared/hooks/use-auth'; import { LikedByUserAtom, SelectedProjectAtom } from '../states'; -import { useNotifications } from '../../../shared/hooks/use-notification'; -import { likeProject } from '../../../infra/rest/apis/like'; +import { useNotifications } from '../../../../../../shared/hooks/use-notification'; +import { likeProject } from '../../../../../../infra/rest/apis/like'; const useProjectInteraction = () => { - const { isAuthenticated } = useAuth(); const [project, setProject] = useAtom(SelectedProjectAtom); const [islikedByUser, setLikedByUser] = useAtom(LikedByUserAtom); const { addNotification } = useNotifications(); const handleLike = async () => { - if (!project?._id || !isAuthenticated()) { - return addNotification({ - message: 'Please login to like this project', - type: 'error', - }); + if (!project?._id) { + return; } try { @@ -37,7 +32,7 @@ const useProjectInteraction = () => { }); } catch (error) { addNotification({ - message: 'Please login to like this project', + message: 'Failed to like this project', type: 'error', }); console.error('Like error:', error); diff --git a/client/src/modules/home/modules/project/v1/index.tsx b/client/src/modules/home/modules/project/v1/index.tsx new file mode 100644 index 00000000..0109ea00 --- /dev/null +++ b/client/src/modules/home/modules/project/v1/index.tsx @@ -0,0 +1,241 @@ +import { useEffect } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { Box, Button, Avatar, Link as MuiLink } from '@mui/material'; +import { getDay } from '../../../../../shared/utils/date'; +import { useAtomValue } from 'jotai'; +import { SelectedProjectAtom } from './states'; +import BannerProjectCard from '../../../v1/components/banner-project-card'; +import { ProjectLoadingSkeleton } from '../../../../../shared/components/atoms/skeleton'; +import { HomePageProjectsAtom } from '../../../v1/states'; +import useProject from './hooks'; +import CommentsWrapper from '../../../../../shared/components/organisms/comments-wrapper'; +import ProjectInteraction from './components/project-interaction'; +import ProjectContent from './components/project-content'; +import { + defaultDarkThumbnail, + defaultLightThumbnail, +} from '../../../../editor/constants'; +import { useA2ZTheme } from '../../../../../shared/hooks/use-theme'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import { getAllProjectsResponse } from '../../../../../infra/rest/apis/project/typing'; +import { OutputBlockData } from '@editorjs/editorjs'; +import { CommentsWrapperAtom } from '../../../../../shared/components/organisms/comments-wrapper/states'; +import A2ZTypography from '../../../../../shared/components/atoms/typography'; + +const Project = () => { + const { project_id } = useParams(); + const { theme: a2zTheme } = useA2ZTheme(); + const selectedProject = useAtomValue(SelectedProjectAtom); + const similarProjects = useAtomValue(HomePageProjectsAtom); + const commentsWrapper = useAtomValue(CommentsWrapperAtom); + const { fetchProject, loading } = useProject(); + + useEffect(() => { + if (project_id) { + fetchProject(project_id); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project_id]); + + if (loading || !selectedProject) { + return ; + } + + return ( + <> + {commentsWrapper && } + + + + {/* Header Section */} + + + + + {selectedProject.live_url && ( + + )} + + {selectedProject.repository_url && ( + + )} + + + + {/* Banner Image */} + + + {/* Author Info + Publish Date */} + + + + + + + @ + + {selectedProject.user_id.personal_info.username} + + + + + + + + + {/* Project Interaction Section */} + + + {/* Project Content */} + + {selectedProject.content_blocks && + selectedProject.content_blocks[0]?.blocks?.map( + (block: OutputBlockData, i: number) => ( + + + + ) + )} + + + + + {/* Similar Projects Section */} + {similarProjects && similarProjects.length > 0 && ( + + + + {similarProjects.map( + (similarProject: getAllProjectsResponse, i: number) => ( + + ) + )} + + )} + + + + ); +}; + +export default Project; diff --git a/client/src/modules/project/states/index.ts b/client/src/modules/home/modules/project/v1/states/index.ts similarity index 65% rename from client/src/modules/project/states/index.ts rename to client/src/modules/home/modules/project/v1/states/index.ts index 075d8ed7..a1c9b056 100644 --- a/client/src/modules/project/states/index.ts +++ b/client/src/modules/home/modules/project/v1/states/index.ts @@ -1,5 +1,5 @@ import { atom } from 'jotai'; -import { ProjectData } from '../../../infra/rest/apis/project/typing'; +import { ProjectData } from '../../../../../../infra/rest/apis/project/typing'; export const SelectedProjectAtom = atom(null); diff --git a/client/src/modules/manage-projects/components/publish-projects.tsx b/client/src/modules/manage-projects/components/publish-projects.tsx index 4da7d8cb..5f0d2c35 100644 --- a/client/src/modules/manage-projects/components/publish-projects.tsx +++ b/client/src/modules/manage-projects/components/publish-projects.tsx @@ -5,6 +5,7 @@ import { useSetAtom } from 'jotai'; import { deleteProjectById } from '../../../infra/rest/apis/project'; import { useNotifications } from '../../../shared/hooks/use-notification'; import { useAuth } from '../../../shared/hooks/use-auth'; +import { ROUTES_V1 } from '../../app/routes/constants/routes'; import { PublishedProjectsAtom, ManageProjectsPaginationState, @@ -152,7 +153,7 @@ const ManagePublishedProjectCard = ({ ) : ( { const { username } = useParams(); const setProfile = useSetAtom(ProfileAtom); - const { fetchProjectsByCategory } = useHome(); + const { fetchProjectsByCategory } = useHomeV1(); const fetchUserProfile = useCallback(async () => { if (!username) return; diff --git a/client/src/modules/profile/index.tsx b/client/src/modules/profile/index.tsx index 74eccace..935c354c 100644 --- a/client/src/modules/profile/index.tsx +++ b/client/src/modules/profile/index.tsx @@ -10,7 +10,7 @@ import { Virtuoso } from 'react-virtuoso'; import { BannerSkeleton } from '../../shared/components/atoms/skeleton'; import { UserAtom } from '../../infra/states/user'; import { Avatar, Box, CircularProgress } from '@mui/material'; -import useHome from '../home/v1/hooks'; +import useHomeV1 from '../home/v1/hooks'; import AboutUser from './components/about-user'; import A2ZTypography from '../../shared/components/atoms/typography'; import Button from '../../shared/components/atoms/button'; @@ -21,7 +21,7 @@ const Profile = () => { const user = useAtomValue(UserAtom); const profile = useAtomValue(ProfileAtom); const [projects, setProjects] = useAtom(HomePageProjectsAtom); - const { fetchProjectsByCategory } = useHome(); + const { fetchProjectsByCategory } = useHomeV1(); const { fetchUserProfile } = useProfile(); useEffect(() => { diff --git a/client/src/modules/project/components/project-content.tsx b/client/src/modules/project/components/project-content.tsx deleted file mode 100644 index c70dc97d..00000000 --- a/client/src/modules/project/components/project-content.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useState } from 'react'; -import { Box, Typography, Button } from '@mui/material'; -import { OutputBlockData } from '@editorjs/editorjs'; - -const ParagraphBlock = ({ text }: { text: string }) => { - return ; -}; - -const HeaderBlock = ({ level, text }: { level: number; text: string }) => { - const Tag = level === 3 ? 'h3' : 'h2'; - const size = level === 3 ? 'h5' : 'h4'; - return ( - - ); -}; - -const ImageBlock = ({ url, caption }: { url: string; caption: string }) => { - return ( - - {caption} - {caption && ( - - {caption} - - )} - - ); -}; - -const QuoteBlock = ({ text, caption }: { text: string; caption: string }) => { - return ( - - - {text} - - {caption && ( - - {caption} - - )} - - ); -}; - -const ListBlock = ({ style, items }: { style: string; items: string[] }) => { - const Tag = style === 'ordered' ? 'ol' : 'ul'; - const styleType = style === 'ordered' ? 'decimal' : 'disc'; - return ( - - {items.map((item, i) => ( -
  • - -
  • - ))} -
    - ); -}; - -const CodeBlock = ({ code, language }: { code: string; language: string }) => { - const codeText = code || ''; - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(codeText); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - setCopied(false); - } - }; - - return ( - - {language ? ( - - {language} - - ) : null} - - - {codeText} - - - ); -}; - -const ProjectContent = ({ block }: { block: OutputBlockData }) => { - const { type, data } = block || {}; - - switch (type) { - case 'paragraph': - return ; - case 'header': - return ; - case 'image': - return ( - - ); - case 'quote': - return ( - - ); - case 'list': - return ; - case 'code': - return ( - - ); - default: - return null; - } -}; - -export default ProjectContent; diff --git a/client/src/modules/project/index.tsx b/client/src/modules/project/index.tsx deleted file mode 100644 index ea5577b3..00000000 --- a/client/src/modules/project/index.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { useEffect } from 'react'; -import { Link, useParams } from 'react-router-dom'; -import { - Box, - Typography, - Button, - Avatar, - Link as MuiLink, -} from '@mui/material'; -import { getDay } from '../../shared/utils/date'; -import { useAtomValue } from 'jotai'; -import { SelectedProjectAtom } from './states'; -import BannerProjectCard from '../home/v1/components/banner-project-card'; -import { ProjectLoadingSkeleton } from '../../shared/components/atoms/skeleton'; -import { HomePageProjectsAtom } from '../home/v1/states'; -import useProject from './hooks'; -import CommentsWrapper from '../../shared/components/organisms/comments-wrapper'; -import ProjectInteraction from './components/project-interaction'; -import ProjectContent from './components/project-content'; -import { - defaultDarkThumbnail, - defaultLightThumbnail, -} from '../editor/constants'; -import { useA2ZTheme } from '../../shared/hooks/use-theme'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import { getAllProjectsResponse } from '../../infra/rest/apis/project/typing'; -import { OutputBlockData } from '@editorjs/editorjs'; -import { CommentsWrapperAtom } from '../../shared/components/organisms/comments-wrapper/states'; - -const Project = () => { - const { project_id } = useParams(); - const { theme: a2zTheme } = useA2ZTheme(); - const selectedProject = useAtomValue(SelectedProjectAtom); - const similarProjects = useAtomValue(HomePageProjectsAtom); - const commentsWrapper = useAtomValue(CommentsWrapperAtom); - const { fetchProject, loading } = useProject(); - - useEffect(() => { - fetchProject(project_id || ''); - }, [project_id, fetchProject]); - - if (loading || !selectedProject) { - return ; - } - - return ( - <> - {commentsWrapper && } - - - {/* Header Section */} - - - {selectedProject.title} - - - - {selectedProject.live_url && ( - - )} - - {selectedProject.repository_url && ( - - )} - - - - {/* Banner Image */} - - - {/* Author Info + Publish Date */} - - - - - {selectedProject.user_id.personal_info.fullname} -
    @ - - {selectedProject.user_id.personal_info.username} - -
    -
    - - - Published on {getDay(selectedProject.publishedAt)} - -
    - - {/* Project Interaction Section */} - - - {/* Project Content */} - - {selectedProject.content_blocks && - selectedProject.content_blocks[0]?.blocks?.map( - (block: OutputBlockData, i: number) => ( - - - - ) - )} - - - - - {/* Similar Projects Section */} - {similarProjects && similarProjects.length > 0 && ( - - - Similar Projects - - - {similarProjects.map( - (similarProject: getAllProjectsResponse, i: number) => ( - - ) - )} - - )} -
    - - ); -}; - -export default Project; diff --git a/client/src/modules/search/index.tsx b/client/src/modules/search/index.tsx index 895f6bf4..3a9e847b 100644 --- a/client/src/modules/search/index.tsx +++ b/client/src/modules/search/index.tsx @@ -7,7 +7,7 @@ import { HomePageProjectsAtom } from '../home/v1/states'; import { Virtuoso } from 'react-virtuoso'; import { BannerSkeleton } from '../../shared/components/atoms/skeleton'; import NoDataMessageBox from '../../shared/components/atoms/no-data-msg'; -import useHome from '../home/v1/hooks'; +import useHomeV1 from '../home/v1/hooks'; import { SearchPageUsersAtom } from './states'; import UserCard from './components/user-card'; import { Box, CircularProgress, Stack } from '@mui/material'; @@ -18,7 +18,7 @@ import PeopleIcon from '@mui/icons-material/People'; const Search = () => { const { query } = useParams(); const projects = useAtomValue(HomePageProjectsAtom); - const { fetchProjectsByCategory } = useHome(); + const { fetchProjectsByCategory } = useHomeV1(); const users = useAtomValue(SearchPageUsersAtom); const { fetchUsers } = useSearch(); diff --git a/client/src/shared/components/organisms/comments-wrapper/components/comment-card.tsx b/client/src/shared/components/organisms/comments-wrapper/components/comment-card.tsx index 05f4f81f..833ca531 100644 --- a/client/src/shared/components/organisms/comments-wrapper/components/comment-card.tsx +++ b/client/src/shared/components/organisms/comments-wrapper/components/comment-card.tsx @@ -17,7 +17,7 @@ import { TotalParentCommentsLoadedAtom, } from '../states'; import { UserAtom } from '../../../../../infra/states/user'; -import { SelectedProjectAtom } from '../../../../../modules/project/states'; +import { SelectedProjectAtom } from '../../../../../modules/home/modules/project/v1/states'; import { useNotifications } from '../../../../hooks/use-notification'; import { deleteComment, diff --git a/client/src/shared/components/organisms/comments-wrapper/components/comment-field.tsx b/client/src/shared/components/organisms/comments-wrapper/components/comment-field.tsx index 262cc42b..dbc4c94b 100644 --- a/client/src/shared/components/organisms/comments-wrapper/components/comment-field.tsx +++ b/client/src/shared/components/organisms/comments-wrapper/components/comment-field.tsx @@ -9,7 +9,7 @@ import { import { useNotifications } from '../../../../hooks/use-notification'; import { UserAtom } from '../../../../../infra/states/user'; import { useAuth } from '../../../../hooks/use-auth'; -import { SelectedProjectAtom } from '../../../../../modules/project/states'; +import { SelectedProjectAtom } from '../../../../../modules/home/modules/project/v1/states'; import { addComment } from '../../../../../infra/rest/apis/comment'; const CommentField = ({ diff --git a/client/src/shared/components/organisms/comments-wrapper/hooks/index.ts b/client/src/shared/components/organisms/comments-wrapper/hooks/index.ts index 5caf07bd..48d28b06 100644 --- a/client/src/shared/components/organisms/comments-wrapper/hooks/index.ts +++ b/client/src/shared/components/organisms/comments-wrapper/hooks/index.ts @@ -1,49 +1,80 @@ -import { useAtom, useAtomValue } from 'jotai'; +import { useCallback, useRef, useEffect } from 'react'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { getComments } from '../../../../../infra/rest/apis/comment'; import { GetCommentsResponse } from '../../../../../infra/rest/apis/comment/typing'; import { AllCommentsAtom, TotalParentCommentsLoadedAtom } from '../states'; -import { SelectedProjectAtom } from '../../../../../modules/project/states'; +import { SelectedProjectAtom } from '../../../../../modules/home/modules/project/v1/states'; const useCommentsWrapper = () => { const [totalParentCommentsLoaded, setTotalParentCommentsLoaded] = useAtom( TotalParentCommentsLoadedAtom ); - const [allComments, setAllComments] = useAtom(AllCommentsAtom); + const setAllComments = useSetAtom(AllCommentsAtom); const selectedProject = useAtomValue(SelectedProjectAtom); + const currentProjectIdRef = useRef(null); - const fetchComments = async ({ project_id }: { project_id: string }) => { - try { - const response = await getComments({ - project_id, - skip: totalParentCommentsLoaded, - }); - if (response.data) { - const transformed = response.data.map(comment => ({ - ...comment, - children_level: 0, - })) as (GetCommentsResponse & { children_level: number })[]; - setTotalParentCommentsLoaded(transformed.length); - - if (transformed.length > 0) { - if (!allComments) { - setAllComments(transformed); + // Reset comments when project changes + useEffect(() => { + if ( + selectedProject?._id && + selectedProject._id !== currentProjectIdRef.current + ) { + currentProjectIdRef.current = selectedProject._id; + setAllComments(null); + setTotalParentCommentsLoaded(0); + } + }, [selectedProject?._id, setAllComments, setTotalParentCommentsLoaded]); + + const fetchComments = useCallback( + async ({ + project_id, + reset = false, + }: { + project_id: string; + reset?: boolean; + }) => { + if (!project_id) return; + + try { + const skip = reset ? 0 : totalParentCommentsLoaded; + const response = await getComments({ + project_id, + skip, + }); + if (response.data) { + const transformed = response.data.map(comment => ({ + ...comment, + children_level: 0, + })) as (GetCommentsResponse & { children_level: number })[]; + + if (reset) { + setTotalParentCommentsLoaded(transformed.length); + setAllComments(transformed.length > 0 ? transformed : null); } else { - setAllComments([...allComments, ...transformed]); + setTotalParentCommentsLoaded(prev => prev + transformed.length); + if (transformed.length > 0) { + setAllComments(prev => { + if (!prev) return transformed; + return [...prev, ...transformed]; + }); + } } } + } catch (err) { + console.error(err); + setAllComments(null); } - } catch (err) { - console.error(err); - setAllComments(null); - } - }; + }, + [totalParentCommentsLoaded, setTotalParentCommentsLoaded, setAllComments] + ); - const loadMoreComments = async () => { + const loadMoreComments = useCallback(async () => { if (!selectedProject?._id) return; await fetchComments({ project_id: selectedProject._id, + reset: false, }); - }; + }, [selectedProject?._id, fetchComments]); return { fetchComments, diff --git a/client/src/shared/components/organisms/comments-wrapper/index.tsx b/client/src/shared/components/organisms/comments-wrapper/index.tsx index 99bf33d0..59d6067a 100644 --- a/client/src/shared/components/organisms/comments-wrapper/index.tsx +++ b/client/src/shared/components/organisms/comments-wrapper/index.tsx @@ -10,7 +10,7 @@ import { } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import NoDataMessage from '../../atoms/no-data-msg'; -import { SelectedProjectAtom } from '../../../../modules/project/states'; +import { SelectedProjectAtom } from '../../../../modules/home/modules/project/v1/states'; import CommentField from './components/comment-field'; import CommentCard from './components/comment-card'; import { From f0922acb016ae2bee56f52ae92a2619a878c36ca Mon Sep 17 00:00:00 2001 From: Avdhesh-Varshney Date: Thu, 8 Jan 2026 00:34:42 +0530 Subject: [PATCH 6/6] format --- client/src/modules/settings/routes/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/modules/settings/routes/index.tsx b/client/src/modules/settings/routes/index.tsx index 8141cfd0..bec90c3e 100644 --- a/client/src/modules/settings/routes/index.tsx +++ b/client/src/modules/settings/routes/index.tsx @@ -1,4 +1,3 @@ - import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined'; import { ROUTES_V1,