diff --git a/client/.vercelignore b/client/.vercelignore
index 051e59ff4..2627ca565 100644
--- a/client/.vercelignore
+++ b/client/.vercelignore
@@ -1,8 +1,6 @@
-projects/*
-server/*
-client/src/modules/edit-profile/*
-client/src/modules/manage-projects/*
-client/src/modules/notification/*
-src/modules/edit-profile/*
-src/modules/manage-projects/*
-src/modules/notification/*
+projects/*
+server/*
+client/src/modules/edit-profile/*
+client/src/modules/notification/*
+src/modules/edit-profile/*
+src/modules/notification/*
diff --git a/client/src/App.tsx b/client/src/App.tsx
index b82e81a15..36bd99e99 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -13,7 +13,7 @@ import Profile from './modules/profile';
import Project from './modules/project';
import Sidebar from './shared/components/organisms/sidebar';
// import ChangePassword from "./modules/change-password";
-// import ManageProjects from "./modules/manage-projects";
+import ManageProjects from './modules/manage-projects';
// import EditProfile from "./modules/edit-profile";
// import Notifications from "./modules/notification";
@@ -35,7 +35,7 @@ function App() {
} />
}>
- {/* } /> */}
+ } />
{/* } /> */}
}>
diff --git a/client/src/modules/manage-projects/components/draft-projects.tsx b/client/src/modules/manage-projects/components/draft-projects.tsx
index 731ac0977..432c8a2a9 100644
--- a/client/src/modules/manage-projects/components/draft-projects.tsx
+++ b/client/src/modules/manage-projects/components/draft-projects.tsx
@@ -1,3 +1,7 @@
+import { Link } from 'react-router-dom';
+import { Box, Typography, Button, Card, CardContent } from '@mui/material';
+import { Project, deleteProject } from './utils';
+
const ManageDraftProjectPost = ({ project }: { project: Project }) => {
const { title, des } = project;
let { index = 0 } = project;
@@ -5,44 +9,53 @@ const ManageDraftProjectPost = ({ project }: { project: Project }) => {
index++;
return (
-
-
- {index < 10 ? '0' + index : index}
-
-
-
-
{title}
-
-
- {des?.length ? des : 'No Description'}
-
-
-
-
- Edit
-
-
-
-
-
-
-
-
-
-
+
+
+ {index < 10 ? '0' + index : index}
+
+
+
+
+
+ {title}
+
+
+
+ {des?.length ? des : 'No Description'}
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/client/src/modules/manage-projects/components/publish-projects.tsx b/client/src/modules/manage-projects/components/publish-projects.tsx
index bcdbfa931..d09a95c52 100644
--- a/client/src/modules/manage-projects/components/publish-projects.tsx
+++ b/client/src/modules/manage-projects/components/publish-projects.tsx
@@ -1,12 +1,18 @@
import { Link } from 'react-router-dom';
-import { getDay } from '../../../../shared/utils/date';
+import { getDay } from '../../../shared/utils/date';
import { useState } from 'react';
import { useAtom } from 'jotai';
-import { UserAtom } from '../../../../shared/states/user';
-import axios from 'axios';
-
-import { SetStateAction } from 'react';
-import { AllProjectsData } from '../../../../infra/rest/typings';
+import { UserAtom } from '../../../infra/states/user';
+import {
+ Box,
+ Typography,
+ Button,
+ Card,
+ CardContent,
+ CardActions,
+ Avatar,
+} from '@mui/material';
+import { Project, deleteProject } from './utils';
interface ProjectStats {
total_likes: number;
@@ -15,168 +21,143 @@ interface ProjectStats {
[key: string]: number; // Allow dynamic key access
}
-interface Project {
- _id?: string;
- project_id: string;
- title: string;
- des?: string;
- banner?: string;
- publishedAt: string;
- activity?: ProjectStats;
- index?: number;
- setStateFunc?: (value: SetStateAction) => void;
-}
+// Project type and deleteProject helper are moved to ./utils to avoid exporting non-component
+// functions from this file (react-refresh requirement).
const ProjectStats = ({ stats }: { stats: ProjectStats }) => {
return (
-
- {Object.keys(stats).map((key, i) => {
- return !key.includes('parent') ? (
-
+ {Object.keys(stats).map((key, i) =>
+ !key.includes('parent') ? (
+
-
- {stats[key].toLocaleString()}
-
-
+ {stats[key].toLocaleString()}
+
{key.split('_')[1]}
-
-
- ) : (
- ''
- );
- })}
-
+
+
+ ) : null
+ )}
+
);
};
-const deleteProject = (
- project: Project,
- access_token: string,
- target: EventTarget | null
-) => {
- const { index, project_id, setStateFunc } = project;
-
- if (!(target instanceof HTMLElement)) return;
-
- target.setAttribute('disabled', 'true');
-
- axios
- .post(
- import.meta.env.VITE_SERVER_DOMAIN + '/api/project/delete',
- { project_id },
- {
- headers: {
- Authorization: `Bearer ${access_token}`,
- },
- }
- )
- .then(() => {
- target.removeAttribute('disabled');
-
- if (setStateFunc) {
- setStateFunc((preVal: AllProjectsData | null) => {
- if (!preVal) return null;
-
- const { deletedDocCount = 0, totalDocs = 0, results = [] } = preVal;
-
- if (
- typeof index === 'number' &&
- index >= 0 &&
- index < results.length
- ) {
- results.splice(index, 1);
- }
-
- const newTotalDocs = totalDocs - 1;
- const newDeletedCount = deletedDocCount + 1;
-
- if (!results.length && newTotalDocs > 0) {
- return null;
- }
-
- return {
- ...preVal,
- results,
- totalDocs: newTotalDocs,
- deletedDocCount: newDeletedCount,
- };
- });
- }
- })
- .catch(err => {
- console.error('Error deleting project:', err);
- target.removeAttribute('disabled');
- });
-};
+// deleteProject is imported from ./utils
const ManagePublishedProjectCard = ({ project }: { project: Project }) => {
const { banner, project_id, title, publishedAt, activity } = project;
const [user] = useAtom(UserAtom);
- const access_token = user.access_token || '';
+ // USER_DB_STATE doesn't include access_token; if present (legacy/session), read safely
+ const access_token = user
+ ? ((user as unknown as { access_token?: string }).access_token ?? '')
+ : '';
const [showStat, setShowStat] = useState(false);
return (
- <>
-
-

-
-
-
-
+
+
+ {banner ? (
+
+ ) : (
+
+ )}
+
+
+
{title}
-
-
- Published on {getDay(publishedAt)}
-
-
-
-
- Edit
-
-
-
-
-
-
-
-
-
-
+
+
+ Published on {getDay(publishedAt)}
+
+
+
+
+
+
+
+
+
+
+ {activity && }
+
+
+
{showStat ? (
-
- ) : (
- ''
- )}
- >
+
+ ) : null}
+
);
};
diff --git a/client/src/modules/manage-projects/components/utils.ts b/client/src/modules/manage-projects/components/utils.ts
new file mode 100644
index 000000000..c71801831
--- /dev/null
+++ b/client/src/modules/manage-projects/components/utils.ts
@@ -0,0 +1,82 @@
+import axios from 'axios';
+import { SetStateAction } from 'react';
+import { AllProjectsData } from '../../../../infra/rest/typings';
+
+interface ProjectStats {
+ total_likes: number;
+ total_comments: number;
+ total_reads: number;
+ [key: string]: number;
+}
+
+export interface Project {
+ _id?: string;
+ project_id: string;
+ title: string;
+ des?: string;
+ banner?: string;
+ publishedAt: string;
+ activity?: ProjectStats;
+ index?: number;
+ setStateFunc?: (value: SetStateAction) => void;
+}
+
+export const deleteProject = (
+ project: Project,
+ access_token: string,
+ target: EventTarget | null
+) => {
+ const { index, project_id, setStateFunc } = project;
+
+ if (!(target instanceof HTMLElement)) return;
+
+ target.setAttribute('disabled', 'true');
+
+ axios
+ .post(
+ import.meta.env.VITE_SERVER_DOMAIN + '/api/project/delete',
+ { project_id },
+ {
+ headers: {
+ Authorization: `Bearer ${access_token}`,
+ },
+ }
+ )
+ .then(() => {
+ target.removeAttribute('disabled');
+
+ if (setStateFunc) {
+ setStateFunc((preVal: AllProjectsData | null) => {
+ if (!preVal) return null;
+
+ const { deletedDocCount = 0, totalDocs = 0, results = [] } = preVal;
+
+ if (
+ typeof index === 'number' &&
+ index >= 0 &&
+ index < results.length
+ ) {
+ results.splice(index, 1);
+ }
+
+ const newTotalDocs = totalDocs - 1;
+ const newDeletedCount = deletedDocCount + 1;
+
+ if (!results.length && newTotalDocs > 0) {
+ return null;
+ }
+
+ return {
+ ...preVal,
+ results,
+ totalDocs: newTotalDocs,
+ deletedDocCount: newDeletedCount,
+ };
+ });
+ }
+ })
+ .catch(err => {
+ console.error('Error deleting project:', err);
+ target.removeAttribute('disabled');
+ });
+};
diff --git a/client/src/modules/manage-projects/index.tsx b/client/src/modules/manage-projects/index.tsx
index 80b68dd12..714eb88f4 100644
--- a/client/src/modules/manage-projects/index.tsx
+++ b/client/src/modules/manage-projects/index.tsx
@@ -1,174 +1,191 @@
-import { useAtom, useAtomValue } from 'jotai';
-import { useEffect, useState, useCallback } from 'react';
-import { useSearchParams } from 'react-router-dom';
+import { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
import InPageNavigation from '../../shared/components/molecules/page-navigation';
-import NoDataMessageBox from '../../shared/components/atoms/no-data-msg';
-import ManagePublishedProjectCard from './components/publish-projects';
-import ManageDraftProjectPost from './components/draft-projects';
-
-const ManageProjects = () => {
- const [projects, setProjects] = useAtom(AllProjectsAtom);
- const [drafts, setDrafts] = useAtom(DraftProjectAtom);
+import NoDataMessage from '../../shared/components/atoms/no-data-msg';
+import { useAtom, useAtomValue } from 'jotai';
+import { HomePageProjectsAtom } from '../home/states';
+import BannerProjectCard from '../home/components/banner-project-card';
+import { ProfileAtom } from '../profile/states';
+import { Virtuoso } from 'react-virtuoso';
+import { BannerSkeleton } from '../../shared/components/atoms/skeleton';
+import { UserAtom } from '../../infra/states/user';
+import { Avatar, Box, CircularProgress, Grid } from '@mui/material';
+import useHome from '../home/hooks';
+import AboutUser from '../profile/components/about-user';
+import A2ZTypography from '../../shared/components/atoms/typography';
+import Button from '../../shared/components/atoms/button';
+import useProfile from '../profile/hooks';
+
+const Profile = () => {
+ const { username } = useParams();
const user = useAtomValue(UserAtom);
-
- const activeTab = useSearchParams()[0].get('tab');
- const [query, setQuery] = useState('');
-
- const getProjects = useCallback(
- (params: Record) => {
- const { page = 1, draft = false, deletedDocCount = 0 } = params;
-
- userWrittenProjects({
- page: page as number,
- draft: draft as boolean,
- query,
- deletedDocCount: deletedDocCount as number,
- })
- .then(async data => {
- const formattedData = (await filterPaginationData({
- state: draft ? drafts : projects,
- data: data.projects || [],
- page: page as number,
- countRoute: '/search-projects-count',
- data_to_send: {
- query,
- tag: query,
- author: user.username || '',
- draft,
- },
- })) as AllProjectsData;
-
- if (formattedData) {
- if (draft) {
- setDrafts(formattedData);
- } else {
- setProjects(formattedData);
- }
- }
- })
- .catch(err => {
- console.log(err);
- });
- },
- [drafts, projects, query, setDrafts, setProjects, user.username]
- );
+ const profile = useAtomValue(ProfileAtom);
+ const [projects, setProjects] = useAtom(HomePageProjectsAtom);
+ const { fetchProjectsByCategory } = useHome();
+ const { fetchUserProfile } = useProfile();
useEffect(() => {
- if (user.access_token) {
- if (projects === null) {
- getProjects({ page: 1, draft: false });
- }
- if (drafts === null) {
- getProjects({ page: 1, draft: true });
- }
- }
- }, [user.access_token, projects, drafts, query, getProjects]);
-
- const handleSearch = (e: React.KeyboardEvent) => {
- const searchQuery = e.currentTarget.value;
- setQuery(searchQuery);
-
- if (e.keyCode === 13 && searchQuery.length) {
- setProjects(null);
- setDrafts(null);
+ if (username !== user?.personal_info.username) {
+ setProjects([]);
}
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- if (!e.currentTarget.value.length) {
- setQuery('');
- setProjects(null);
- setDrafts(null);
+ if (projects.length === 0 || profile?.personal_info.username !== username) {
+ fetchUserProfile();
}
- };
+ }, [
+ username,
+ user?.personal_info.username,
+ projects.length,
+ profile?.personal_info.username,
+ fetchUserProfile,
+ setProjects,
+ ]);
+
+ if (!profile) {
+ return (
+
+
+
+ );
+ }
return (
- <>
- Manage Projects
-
-
-
-
-
-
-
+
- {
- // Published Projects
- projects === null ? (
-
- ) : projects.results.length ? (
- <>
- {projects.results.map((project, i) => {
- return (
-
- );
- })}
-
- ({ md: `1px solid ${theme.palette.divider}` }),
+ }}
+ >
+
+
+
+
+
+
+ {user && username === user.personal_info.username ? (
+
+ ) : undefined}
+
+
+
+
+
+
+ {/* Left Column (Projects + About Tabs) */}
+
+
+ {projects.length ? (
+
- >
- ) : (
-
- )
- }
-
- {
- // Draft Projects
- drafts === null ? (
-
- ) : drafts.results.length ? (
- <>
- {drafts.results.map((project, i) => {
- return (
-
- );
- })}
-
- (
+
+ )}
+ overscan={200}
+ endReached={() => {
+ const nextPage = Math.floor(projects.length / 10) + 1; // Assuming page size of 10
+ fetchProjectsByCategory({
+ page: nextPage,
+ user_id: profile._id,
+ });
+ }}
+ components={{
+ Footer: () =>
+ !projects || projects.length === 0 ? (
+
+ ) : null, // FIX ME
}}
/>
- >
- ) : (
-
- )
- }
-
- >
+ ) : (
+
+ )}
+
+
+
+
+
+
);
};
-export default ManageProjects;
+export default Profile;
diff --git a/client/src/modules/manage-projects/states/index.ts b/client/src/modules/manage-projects/states/index.ts
index e69de29bb..6dfd1c4f0 100644
--- a/client/src/modules/manage-projects/states/index.ts
+++ b/client/src/modules/manage-projects/states/index.ts
@@ -0,0 +1,13 @@
+import { atom } from 'jotai';
+import { PROJECT_DB_STATE } from '../../../infra/rest/typings';
+
+// Minimal pagination type used across the app
+export interface PaginatedProjects {
+ results: PROJECT_DB_STATE[];
+ totalDocs: number;
+ deletedDocCount?: number;
+ page?: number;
+}
+
+export const AllProjectsAtom = atom(null);
+export const DraftProjectAtom = atom(null);
diff --git a/client/src/modules/project/components/project-content.tsx b/client/src/modules/project/components/project-content.tsx
index c70dc97d1..38f520d90 100644
--- a/client/src/modules/project/components/project-content.tsx
+++ b/client/src/modules/project/components/project-content.tsx
@@ -1,25 +1,37 @@
-import { useState } from 'react';
-import { Box, Typography, Button } from '@mui/material';
import { OutputBlockData } from '@editorjs/editorjs';
+import React, { useState } from 'react';
+import { Box, Typography, Button } from '@mui/material';
+
+interface ProjectContentProps {
+ block: OutputBlockData;
+}
+
+type EJData = Record | undefined;
-const ParagraphBlock = ({ text }: { text: string }) => {
+const ParagraphBlock = ({ data }: { data?: EJData }) => {
+ const text = (data && (data['text'] as string)) || '';
return ;
};
-const HeaderBlock = ({ level, text }: { level: number; text: string }) => {
+const HeaderBlock = ({ data }: { data?: EJData }) => {
+ const level = (data && (data['level'] as number)) || 2;
const Tag = level === 3 ? 'h3' : 'h2';
const size = level === 3 ? 'h5' : 'h4';
+ const text = (data && (data['text'] as string)) || '';
return (
);
};
-const ImageBlock = ({ url, caption }: { url: string; caption: string }) => {
+const ImageBlock = ({ data }: { data?: EJData }) => {
+ const file = data && (data['file'] as EJData);
+ const url = (file && (file['url'] as string)) || '';
+ const caption = (data && (data['caption'] as string)) || '';
return (
{
alt={caption}
style={{ maxWidth: '100%', borderRadius: '8px' }}
/>
- {caption && (
+ {caption ? (
{caption}
- )}
+ ) : null}
);
};
-const QuoteBlock = ({ text, caption }: { text: string; caption: string }) => {
+const QuoteBlock = ({ data }: { data?: EJData }) => {
+ const text = (data && (data['text'] as string)) || '';
+ const caption = (data && (data['caption'] as string)) || '';
return (
{
{text}
- {caption && (
+ {caption ? (
{caption}
- )}
+ ) : null}
);
};
-const ListBlock = ({ style, items }: { style: string; items: string[] }) => {
+const ListBlock = ({ data }: { data?: EJData }) => {
+ const style = (data && (data['style'] as string)) || 'unordered';
const Tag = style === 'ordered' ? 'ol' : 'ul';
const styleType = style === 'ordered' ? 'decimal' : 'disc';
+ const items = (data && (data['items'] as string[])) || [];
return (
-
+
{items.map((item, i) => (
{
);
};
-const CodeBlock = ({ code, language }: { code: string; language: string }) => {
- const codeText = code || '';
+const CodeBlock = ({ data }: { data?: EJData }) => {
+ const codeText = (data && (data['code'] as string)) || '';
+ const language = (data && (data['language'] as string)) || '';
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@@ -132,28 +153,22 @@ const CodeBlock = ({ code, language }: { code: string; language: string }) => {
);
};
-const ProjectContent = ({ block }: { block: OutputBlockData }) => {
- const { type, data } = block || {};
+const ProjectContent = ({ block }: ProjectContentProps) => {
+ const { type, data } = block;
switch (type) {
case 'paragraph':
- return ;
+ return ;
case 'header':
- return ;
+ return ;
case 'image':
- return (
-
- );
+ return ;
case 'quote':
- return (
-
- );
+ return ;
case 'list':
- return ;
+ return ;
case 'code':
- return (
-
- );
+ return ;
default:
return null;
}
diff --git a/client/src/shared/components/organisms/sidebar/index.tsx b/client/src/shared/components/organisms/sidebar/index.tsx
index 7e2a79846..84c5b8407 100644
--- a/client/src/shared/components/organisms/sidebar/index.tsx
+++ b/client/src/shared/components/organisms/sidebar/index.tsx
@@ -1,7 +1,5 @@
-import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
import { Navigate, NavLink, Outlet, useLocation } from 'react-router-dom';
-import { UserAtom } from '../../../../infra/states/user';
import { useAuth } from '../../../hooks/use-auth';
import { notificationStatus } from '../../../../infra/rest/apis/notification';
@@ -19,6 +17,7 @@ import {
Typography,
useMediaQuery,
useTheme,
+ CircularProgress,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
@@ -30,8 +29,7 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
const drawerWidth = 240;
const Sidebar = () => {
- const user = useAtomValue(UserAtom);
- const { isAuthenticated } = useAuth();
+ const { isAuthenticated, initialized } = useAuth();
const location = useLocation();
const page = location.pathname.split('/')[2];
@@ -43,7 +41,23 @@ const Sidebar = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ // TEMP DEBUG: log auth init and token presence to help diagnose routing issues
useEffect(() => {
+ try {
+ console.debug('Sidebar debug:', {
+ initialized,
+ isAuthenticated: isAuthenticated(),
+ token: localStorage.getItem('access_token'),
+ });
+ } catch (err) {
+ console.debug('Sidebar debug error', err);
+ }
+ }, [initialized]);
+
+ useEffect(() => {
+ // Only fetch notification status after auth initialization and when authenticated.
+ if (!initialized || !isAuthenticated()) return;
+
const fetchNotificationStatus = async () => {
const response = await notificationStatus();
if (response.status === 'success' && response.data) {
@@ -51,9 +65,27 @@ const Sidebar = () => {
}
};
fetchNotificationStatus();
- }, []);
+ }, [initialized, isAuthenticated]);
+
+ // If auth hasn't finished initializing yet, render a small loader to avoid
+ // premature redirects while the token/user is being restored.
+ if (!initialized) {
+ return (
+
+
+
+ );
+ }
- if (!user || !isAuthenticated()) {
+ // After initialization, if not authenticated, redirect to login.
+ if (!isAuthenticated()) {
return ;
}