diff --git a/client/src/app/routes/constants/routes.ts b/client/src/app/routes/constants/routes.ts index cba1d32e..5c692760 100644 --- a/client/src/app/routes/constants/routes.ts +++ b/client/src/app/routes/constants/routes.ts @@ -10,10 +10,13 @@ export enum ROUTES_V1 { export enum ROUTES_HOME_V1 { PROJECT = '/:project_id', + EDITOR = '/editor', + EDITOR_WITH_ID = '/editor/:project_id', } export enum ROUTES_SETTINGS_V1 { PROFILE = '/profile', + MANAGE_ARTICLES = '/manage-articles', INTEGRATIONS = '/integrations', } diff --git a/client/src/modules/editor/components/editor-navbar.tsx b/client/src/modules/editor/components/editor-navbar.tsx deleted file mode 100644 index 6e561f90..00000000 --- a/client/src/modules/editor/components/editor-navbar.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { AppBar, Toolbar, Box } from '@mui/material'; -import { Link } from 'react-router-dom'; -import { useTheme } from '@mui/material/styles'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import A2ZTypography from '../../../shared/components/atoms/typography'; -import A2ZButton from '../../../shared/components/atoms/button'; - -const EditorNavbar = ({ - title, - onSaveDraft, - onPublish, -}: { - title?: string; - onSaveDraft: () => void; - onPublish: () => void; -}) => { - const theme = useTheme(); - - return ( - - - - - - - - - - - Publish - - - - Save Draft - - - - - ); -}; - -export default EditorNavbar; diff --git a/client/src/modules/editor/components/tags.tsx b/client/src/modules/editor/components/tags.tsx deleted file mode 100644 index 8856c580..00000000 --- a/client/src/modules/editor/components/tags.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Tags = () => { - return
Tags Component
; -}; - -export default Tags; diff --git a/client/src/modules/editor/constants/index.ts b/client/src/modules/editor/constants/index.ts deleted file mode 100644 index 3d60d77a..00000000 --- a/client/src/modules/editor/constants/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const characterLimit = 200; -export const tagLimit = 10; - -export const defaultLightThumbnail = - 'https://res.cloudinary.com/avdhesh-varshney/image/upload/v1761476229/OS7sL4eIr_MiI9fqvkYCw-1761476227343.jpg'; - -export const defaultDarkThumbnail = - 'https://res.cloudinary.com/avdhesh-varshney/image/upload/v1761476020/FHX9kToS3GiG0sgUFQ_Mo-1761476018134.jpg'; diff --git a/client/src/modules/editor/index.tsx b/client/src/modules/editor/index.tsx deleted file mode 100644 index ebd52b18..00000000 --- a/client/src/modules/editor/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Navigate } from 'react-router-dom'; -import { useAuth } from '../../shared/hooks/use-auth'; -import { EditorPageState } from './states'; -import { CircularProgress } from '@mui/material'; -import ProjectEditor from './components/project-editor'; -import PublishForm from './components/publish-form'; -import { useEditor } from './hooks'; - -const Editor = () => { - const { isAuthenticated, initialized } = useAuth(); - const { loading, editorPageState } = useEditor(); - - if (!initialized || loading) return ; - if (!isAuthenticated()) return ; - - return editorPageState === EditorPageState.EDITOR ? ( - - ) : editorPageState === EditorPageState.PUBLISH ? ( - - ) : undefined; -}; - -export default Editor; diff --git a/client/src/modules/editor/components/project-editor.tsx b/client/src/modules/home/modules/editor/v1/components/project-editor.tsx similarity index 61% rename from client/src/modules/editor/components/project-editor.tsx rename to client/src/modules/home/modules/editor/v1/components/project-editor.tsx index c4677c34..9e9a8efc 100644 --- a/client/src/modules/editor/components/project-editor.tsx +++ b/client/src/modules/home/modules/editor/v1/components/project-editor.tsx @@ -2,14 +2,24 @@ import { Box, Divider, useTheme } from '@mui/material'; import TitleIcon from '@mui/icons-material/Title'; import LinkIcon from '@mui/icons-material/Link'; import HttpIcon from '@mui/icons-material/Http'; -import { defaultLightThumbnail, defaultDarkThumbnail } from '../constants'; -import EditorNavbar from './editor-navbar'; -import { useA2ZTheme } from '../../../shared/hooks/use-theme'; -import InputBox from '../../../shared/components/atoms/input-box'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import PublishIcon from '@mui/icons-material/Publish'; +import SaveIcon from '@mui/icons-material/Save'; +import { Link } from 'react-router-dom'; +import { + defaultDarkThumbnail, + defaultLightThumbnail, +} from '../../../../../../shared/constants'; +import Header from '../../../../../../shared/components/organisms/header'; +import { useA2ZTheme } from '../../../../../../shared/hooks/use-theme'; +import InputBox from '../../../../../../shared/components/atoms/input-box'; import A2ZTextEditor from './text-editor'; import { useProjectEditor } from '../hooks/use-project-editor'; import { useAtomValue } from 'jotai'; import { EditorContentAtom } from '../states'; +import A2ZTypography from '../../../../../../shared/components/atoms/typography'; +import A2ZButton from '../../../../../../shared/components/atoms/button'; +import { ROUTES_V1 } from '../../../../../../app/routes/constants/routes'; const ProjectEditor = () => { const theme = useTheme(); @@ -26,10 +36,80 @@ const ProjectEditor = () => { return ( <> - + + + + + + + } + rightSideActions={[ + { + key: 'publish', + label: 'Publish', + icon: , + mobileIcon: , + onClick: handlePublishEvent, + desktopNode: ( + + Publish + + ), + }, + { + key: 'save-draft', + label: 'Save Draft', + icon: , + mobileIcon: , + onClick: handleDraftProject, + desktopNode: ( + + Save Draft + + ), + }, + ]} /> { const theme = useTheme(); @@ -156,7 +155,7 @@ const PublishForm = () => { /> { }} /> { { return await uploadImage(e).then(({ data }) => { diff --git a/client/src/modules/home/modules/editor/v1/constants/index.ts b/client/src/modules/home/modules/editor/v1/constants/index.ts new file mode 100644 index 00000000..e1acaac6 --- /dev/null +++ b/client/src/modules/home/modules/editor/v1/constants/index.ts @@ -0,0 +1,2 @@ +export const CHARACTER_LIMIT = 200; +export const TAG_LIMIT = 10; diff --git a/client/src/modules/editor/hooks/index.ts b/client/src/modules/home/modules/editor/v1/hooks/index.ts similarity index 79% rename from client/src/modules/editor/hooks/index.ts rename to client/src/modules/home/modules/editor/v1/hooks/index.ts index c6588b06..9ed89669 100644 --- a/client/src/modules/editor/hooks/index.ts +++ b/client/src/modules/home/modules/editor/v1/hooks/index.ts @@ -1,17 +1,13 @@ -import { useState } from 'react'; import { useAtom } from 'jotai'; import { EditorPageAtom, EditorPageState } from '../states'; export const useEditor = () => { - const [loading, setLoading] = useState(false); const [editorPageState, setEditorPageState] = useAtom(EditorPageAtom); const handlePublish = () => setEditorPageState(EditorPageState.PUBLISH); const handleBackToEditor = () => setEditorPageState(EditorPageState.EDITOR); return { - loading, - setLoading, editorPageState, setEditorPageState, handlePublish, diff --git a/client/src/modules/editor/hooks/use-project-editor.ts b/client/src/modules/home/modules/editor/v1/hooks/use-project-editor.ts similarity index 80% rename from client/src/modules/editor/hooks/use-project-editor.ts rename to client/src/modules/home/modules/editor/v1/hooks/use-project-editor.ts index ef607e97..4562bcf9 100644 --- a/client/src/modules/editor/hooks/use-project-editor.ts +++ b/client/src/modules/home/modules/editor/v1/hooks/use-project-editor.ts @@ -1,18 +1,22 @@ -import { uploadImage } from '../../../infra/rest/apis/media'; -import { useNotifications } from '../../../shared/hooks/use-notification'; +import { uploadImage } from '../../../../../../infra/rest/apis/media'; +import { useNotifications } from '../../../../../../shared/hooks/use-notification'; import { useAtom, useSetAtom } from 'jotai'; import { - EditorContent, EditorContentAtom, EditorPageAtom, EditorPageState, TextEditorAtom, + DEFAULT_EDITOR_CONTENT, } from '../states'; import { OutputData } from '@editorjs/editorjs'; -import { createProject } from '../../../infra/rest/apis/project'; +import { createProject } from '../../../../../../infra/rest/apis/project'; import { useNavigate, useParams } from 'react-router-dom'; import { useEditor } from '.'; -import { tagLimit } from '../constants'; +import { TAG_LIMIT } from '../constants'; +import { + ROUTES_SETTINGS_V1, + ROUTES_V1, +} from '../../../../../../app/routes/constants/routes'; export const useProjectEditor = () => { const { addNotification } = useNotifications(); @@ -34,9 +38,7 @@ export const useProjectEditor = () => { message, type: status, }); - setEditorContent( - prev => ({ ...prev, banner: data.upload_url }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, banner: data.upload_url })); } }) .catch(err => { @@ -47,56 +49,43 @@ export const useProjectEditor = () => { const handleTitleChange = (e: React.ChangeEvent) => { const textarea = e.target; - setEditorContent( - prev => ({ ...prev, title: textarea.value }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, title: textarea.value })); }; const handleDescriptionChange = ( e: React.ChangeEvent ) => { const textarea = e.target; - setEditorContent( - prev => ({ ...prev, description: textarea.value }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, description: textarea.value })); }; const handleRepositoryURLChange = ( e: React.ChangeEvent ) => { const input = e.target; - setEditorContent( - prev => ({ ...prev, repositoryURL: input.value }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, repositoryURL: input.value })); }; const handleLiveURLChange = (e: React.ChangeEvent) => { const input = e.target; - setEditorContent( - prev => ({ ...prev, liveURL: input.value }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, liveURL: input.value })); }; const handleTagsAdd = (tag: string) => { - if (editorContent?.tags && editorContent.tags.length >= tagLimit) { + if (editorContent?.tags && editorContent.tags.length >= TAG_LIMIT) { return addNotification({ - message: `You can add up to ${tagLimit} tags only`, + message: `You can add up to ${TAG_LIMIT} tags only`, type: 'error', }); } - setEditorContent( - prev => ({ ...prev, tags: [...(prev?.tags || []), tag] }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, tags: [...prev.tags, tag] })); }; const handleTagsDelete = (index: number) => { - setEditorContent( - prev => - ({ - ...prev, - tags: prev?.tags.filter((_, i) => i !== index), - }) as EditorContent - ); + setEditorContent(prev => ({ + ...prev, + tags: prev.tags.filter((_, i) => i !== index), + })); }; const handleDraftProject = async () => { @@ -117,9 +106,7 @@ export const useProjectEditor = () => { textEditor.editor ?.save() .then(async (outputData: OutputData) => { - setEditorContent( - prev => ({ ...prev, content: outputData }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, content: outputData })); const response = await createProject({ _id: project_id ?? undefined, title: editorContent.title, @@ -139,7 +126,7 @@ export const useProjectEditor = () => { setTimeout(() => { navigate('/dashboard/projects?tab=draft'); }, 500); - setEditorContent(null); + setEditorContent(DEFAULT_EDITOR_CONTENT); setTextEditor({ isReady: false }); setEditorPageState(EditorPageState.EDITOR); } @@ -168,9 +155,7 @@ export const useProjectEditor = () => { ?.save() .then((outputData: OutputData) => { if (outputData.blocks.length) { - setEditorContent( - prev => ({ ...prev, content: outputData }) as EditorContent - ); + setEditorContent(prev => ({ ...prev, content: outputData })); handlePublish(); } else { return addNotification({ @@ -246,9 +231,9 @@ export const useProjectEditor = () => { type: response.status, }); setTimeout(() => { - navigate('/dashboard/projects?tab=published'); + navigate(`${ROUTES_V1.SETTINGS}${ROUTES_SETTINGS_V1.MANAGE_ARTICLES}`); }, 500); - setEditorContent(null); + setEditorContent(DEFAULT_EDITOR_CONTENT); setTextEditor({ isReady: false }); setEditorPageState(EditorPageState.EDITOR); } diff --git a/client/src/modules/home/modules/editor/v1/index.tsx b/client/src/modules/home/modules/editor/v1/index.tsx new file mode 100644 index 00000000..fbd02fb1 --- /dev/null +++ b/client/src/modules/home/modules/editor/v1/index.tsx @@ -0,0 +1,16 @@ +import { EditorPageState } from './states'; +import ProjectEditor from './components/project-editor'; +import PublishForm from './components/publish-form'; +import { useEditor } from './hooks'; + +const Editor = () => { + const { editorPageState } = useEditor(); + + if (editorPageState === EditorPageState.PUBLISH) { + return ; + } + + return ; +}; + +export default Editor; diff --git a/client/src/modules/editor/states/index.ts b/client/src/modules/home/modules/editor/v1/states/index.ts similarity index 67% rename from client/src/modules/editor/states/index.ts rename to client/src/modules/home/modules/editor/v1/states/index.ts index f2fda462..bc2718bf 100644 --- a/client/src/modules/editor/states/index.ts +++ b/client/src/modules/home/modules/editor/v1/states/index.ts @@ -16,10 +16,20 @@ export interface EditorContent { repositoryURL: string; } +export const DEFAULT_EDITOR_CONTENT: EditorContent = { + title: '', + description: '', + banner: '', + content: { blocks: [] }, + tags: [], + liveURL: '', + repositoryURL: '', +}; + export const EditorPageAtom = atom(EditorPageState.EDITOR); export const TextEditorAtom = atom<{ isReady: boolean; editor?: EditorJS }>({ isReady: false, }); -export const EditorContentAtom = atom(null); +export const EditorContentAtom = atom(DEFAULT_EDITOR_CONTENT); diff --git a/client/src/modules/home/modules/index.tsx b/client/src/modules/home/modules/index.tsx index 6392128d..d5441ee3 100644 --- a/client/src/modules/home/modules/index.tsx +++ b/client/src/modules/home/modules/index.tsx @@ -2,3 +2,4 @@ import { lazy } from 'react'; export const ProjectLazyComponentV1 = lazy(() => import('./project/v1')); export const SearchLazyComponentV1 = lazy(() => import('./search/v1')); +export const EditorLazyComponentV1 = lazy(() => import('./editor/v1')); diff --git a/client/src/modules/home/modules/project/v1/components/project-interaction.tsx b/client/src/modules/home/modules/project/v1/components/project-interaction.tsx index 89f2ee30..16e22a20 100644 --- a/client/src/modules/home/modules/project/v1/components/project-interaction.tsx +++ b/client/src/modules/home/modules/project/v1/components/project-interaction.tsx @@ -13,6 +13,10 @@ import { likeStatus } from '../../../../../../infra/rest/apis/like'; import useProjectInteraction from '../hooks/use-project-interaction'; import { CommentsWrapperAtom } from '../../../../../../shared/components/organisms/comments-wrapper/states'; import A2ZTypography from '../../../../../../shared/components/atoms/typography'; +import { + ROUTES_V1, + ROUTES_HOME_V1, +} from '../../../../../../app/routes/constants/routes'; // Module-level refs to prevent duplicate API calls across component instances const fetchedProjectIdRef: { current: string | null } = { current: null }; @@ -131,7 +135,7 @@ const ProjectInteraction = () => { { const routes: React.ReactNode[] = [ @@ -12,6 +12,20 @@ export const homeRoutes = () => { } />, + + } + />, + + } + />, ]; return { routes }; 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 7a76f14d..c6af638a 100644 --- a/client/src/modules/home/v1/components/banner-project-card.tsx +++ b/client/src/modules/home/v1/components/banner-project-card.tsx @@ -7,7 +7,7 @@ import { getDay } from '../../../../shared/utils/date'; import { defaultDarkThumbnail, defaultLightThumbnail, -} from '../../../editor/constants'; +} from '../../../../shared/constants'; import { getAllProjectsResponse } from '../../../../infra/rest/apis/project/typing'; import A2ZTypography from '../../../../shared/components/atoms/typography'; import { ROUTES_V1 } from '../../../../app/routes/constants/routes'; diff --git a/client/src/modules/home/v1/index.tsx b/client/src/modules/home/v1/index.tsx index f2f947c5..7f92c6bd 100644 --- a/client/src/modules/home/v1/index.tsx +++ b/client/src/modules/home/v1/index.tsx @@ -13,6 +13,10 @@ import { SearchLazyComponentV1 } from '../modules'; import useSearchV1 from '../modules/search/v1/hooks'; import SubscribeModal from './components/subscribe-modal'; import { useSubscribe } from './hooks/use-subscribe'; +import { + ROUTES_V1, + ROUTES_HOME_V1, +} from '../../../app/routes/constants/routes'; const Home = () => { const setProjects = useSetAtom(HomePageProjectsAtom); @@ -49,7 +53,7 @@ const Home = () => { ), - link: '/editor', + link: `${ROUTES_V1.HOME}${ROUTES_HOME_V1.EDITOR}`, }, { key: 'subscribe', diff --git a/client/src/modules/manage-projects/index.tsx b/client/src/modules/manage-projects/index.tsx deleted file mode 100644 index 39ebf739..00000000 --- a/client/src/modules/manage-projects/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useSearchParams } 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'; -import useManageProjects from './hooks'; -import { Box, CircularProgress } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import InputBox from '../../shared/components/atoms/input-box'; -import A2ZTypography from '../../shared/components/atoms/typography'; -import Button from '../../shared/components/atoms/button'; -import { useSetAtom } from 'jotai'; -import { PublishedProjectsAtom, DraftProjectsAtom } from './states'; - -const ManageProjects = () => { - const [searchParams] = useSearchParams(); - const activeTab = searchParams.get('tab'); - const [query, setQuery] = useState(''); - const { fetchProjects, publishedProjects, draftProjects } = - useManageProjects(); - const setPublishedProjects = useSetAtom(PublishedProjectsAtom); - const setDraftProjects = useSetAtom(DraftProjectsAtom); - - useEffect(() => { - if (publishedProjects === null) { - fetchProjects({ page: 1, is_draft: false, query }); - } - if (draftProjects === null) { - fetchProjects({ page: 1, is_draft: true, query }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [publishedProjects, draftProjects, query]); - - const handleSearch = (e: React.KeyboardEvent) => { - const value = e.currentTarget.value; - if (e.key === 'Enter' && value.length) { - setPublishedProjects(null); - setDraftProjects(null); - } - }; - - const handleChange = (e: React.ChangeEvent) => { - const value = e.currentTarget.value; - setQuery(value); - if (!value.length) { - setPublishedProjects(null); - setDraftProjects(null); - } - }; - - const handleLoadMore = (is_draft: boolean) => { - const currentState = is_draft ? draftProjects : publishedProjects; - if (currentState && currentState.results.length < currentState.totalDocs) { - fetchProjects({ - page: currentState.page + 1, - is_draft, - query, - deletedDocCount: currentState.deletedDocCount || 0, - }); - } - }; - - return ( - - - - - } - sx={{ width: '100%' }} - slotProps={{ - htmlInput: { - onChange: handleChange, - onKeyDown: handleSearch, - }, - }} - /> - - - - {/* Published Projects */} - {publishedProjects === null ? ( - - - - ) : publishedProjects.results.length ? ( - <> - {publishedProjects.results.map((project, i) => ( - - ))} - - {publishedProjects.results.length < publishedProjects.totalDocs && ( - - - - )} - - ) : ( - - )} - - {/* Draft Projects */} - {draftProjects === null ? ( - - - - ) : draftProjects.results.length ? ( - <> - {draftProjects.results.map((project, i) => ( - - ))} - - {draftProjects.results.length < draftProjects.totalDocs && ( - - - - )} - - ) : ( - - )} - - - ); -}; - -export default ManageProjects; diff --git a/client/src/modules/settings/modules/index.tsx b/client/src/modules/settings/modules/index.tsx index a00cd769..1a8ffba3 100644 --- a/client/src/modules/settings/modules/index.tsx +++ b/client/src/modules/settings/modules/index.tsx @@ -7,3 +7,6 @@ export const IntegrationsLazyComponentV1 = lazy( export const OpenAILazyComponentV1 = lazy( () => import('./integrations/modules/openai/v1') ); +export const ManageArticlesLazyComponentV1 = lazy( + () => import('./manage-articles/v1') +); diff --git a/client/src/modules/manage-projects/components/draft-projects.tsx b/client/src/modules/settings/modules/manage-articles/v1/components/draft-articles.tsx similarity index 71% rename from client/src/modules/manage-projects/components/draft-projects.tsx rename to client/src/modules/settings/modules/manage-articles/v1/components/draft-articles.tsx index e3f59921..c8dd9d88 100644 --- a/client/src/modules/manage-projects/components/draft-projects.tsx +++ b/client/src/modules/settings/modules/manage-articles/v1/components/draft-articles.tsx @@ -1,9 +1,13 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; +import { + ROUTES_HOME_V1, + ROUTES_V1, +} from '../../../../../../app/routes/constants/routes'; 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 { deleteProjectById } from '../../../../../../infra/rest/apis/project'; +import { useNotifications } from '../../../../../../shared/hooks/use-notification'; +import { useAuth } from '../../../../../../shared/hooks/use-auth'; import { DraftProjectsAtom, ManageProjectsPaginationState } from '../states'; import { Box, @@ -15,17 +19,14 @@ import { } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; -import { userProjectsResponse } from '../../../infra/rest/apis/project/typing'; +import { userProjectsResponse } from '../../../../../../infra/rest/apis/project/typing'; -interface ManageDraftProjectPostProps { +interface ManageDraftArticleProps { project: userProjectsResponse; index: number; } -const ManageDraftProjectPost = ({ - project, - index, -}: ManageDraftProjectPostProps) => { +const ManageDraftArticle = ({ project, index }: ManageDraftArticleProps) => { const { _id, title, description } = project; const setDraftProjects = useSetAtom(DraftProjectsAtom); const { addNotification } = useNotifications(); @@ -77,10 +78,25 @@ const ManageDraftProjectPost = ({ mb: 3, border: 1, borderColor: 'divider', + borderRadius: 3, + bgcolor: 'background.default', + boxShadow: theme => theme.shadows[1], + transition: 'transform 0.2s ease, box-shadow 0.2s ease', + '&:hover': { + transform: 'translateY(-2px)', + boxShadow: theme => theme.shadows[3], + }, }} > - - + + @@ -151,4 +167,4 @@ const ManageDraftProjectPost = ({ ); }; -export default ManageDraftProjectPost; +export default ManageDraftArticle; diff --git a/client/src/modules/manage-projects/components/publish-projects.tsx b/client/src/modules/settings/modules/manage-articles/v1/components/publish-articles.tsx similarity index 74% rename from client/src/modules/manage-projects/components/publish-projects.tsx rename to client/src/modules/settings/modules/manage-articles/v1/components/publish-articles.tsx index b5e81a1e..aa77f1a2 100644 --- a/client/src/modules/manage-projects/components/publish-projects.tsx +++ b/client/src/modules/settings/modules/manage-articles/v1/components/publish-articles.tsx @@ -1,11 +1,14 @@ import { Link } from 'react-router-dom'; -import { getDay } from '../../../shared/utils/date'; +import { getDay } from '../../../../../../shared/utils/date'; import { useState } from 'react'; 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 { deleteProjectById } from '../../../../../../infra/rest/apis/project'; +import { useNotifications } from '../../../../../../shared/hooks/use-notification'; +import { useAuth } from '../../../../../../shared/hooks/use-auth'; +import { + ROUTES_HOME_V1, + ROUTES_V1, +} from '../../../../../../app/routes/constants/routes'; import { PublishedProjectsAtom, ManageProjectsPaginationState, @@ -24,15 +27,15 @@ import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { userProjectsResponse } from '../../../infra/rest/apis/project/typing'; -import { PROJECT_ACTIVITY } from '../../../infra/rest/typings'; +import { userProjectsResponse } from '../../../../../../infra/rest/apis/project/typing'; +import { PROJECT_ACTIVITY } from '../../../../../../infra/rest/typings'; -interface ManagePublishedProjectCardProps { +interface ManagePublishedArticleProps { project: userProjectsResponse; index: number; } -const ProjectStats = ({ activity }: { activity: PROJECT_ACTIVITY }) => { +const ArticleStats = ({ activity }: { activity: PROJECT_ACTIVITY }) => { return ( { ); }; -const ManagePublishedProjectCard = ({ +const ManagePublishedArticle = ({ project, index, -}: ManagePublishedProjectCardProps) => { +}: ManagePublishedArticleProps) => { const { _id, title, banner_url, publishedAt, activity } = project; const setPublishedProjects = useSetAtom(PublishedProjectsAtom); const { addNotification } = useNotifications(); @@ -104,13 +107,13 @@ const ManagePublishedProjectCard = ({ }; }); addNotification({ - message: 'Project deleted successfully', + message: 'Article deleted successfully', type: 'success', }); } catch (error: unknown) { const err = error as { response?: { data?: { error?: string } } }; addNotification({ - message: err.response?.data?.error || 'Failed to delete project', + message: err.response?.data?.error || 'Failed to delete article', type: 'error', }); } finally { @@ -124,14 +127,23 @@ const ManagePublishedProjectCard = ({ mb: 3, border: 1, borderColor: 'divider', + borderRadius: 3, + bgcolor: 'background.default', + boxShadow: theme => theme.shadows[1], + transition: 'transform 0.2s ease, box-shadow 0.2s ease', + '&:hover': { + transform: 'translateY(-2px)', + boxShadow: theme => theme.shadows[3], + }, }} > - + {banner_url && ( @@ -140,11 +152,11 @@ const ManagePublishedProjectCard = ({ src={banner_url} alt={title} sx={{ - width: { xs: '100%', xl: 112 }, - height: { xs: 200, xl: 112 }, + width: { xs: '100%', sm: 180, md: 140 }, + height: { xs: 180, sm: 120, md: 112 }, objectFit: 'cover', borderRadius: 2, - display: { xs: 'block', lg: 'none', xl: 'block' }, + border: theme => `1px solid ${theme.palette.divider}`, }} /> )} @@ -172,16 +184,16 @@ const ManagePublishedProjectCard = ({ @@ -211,14 +223,14 @@ const ManagePublishedProjectCard = ({ {activity && ( - + )} - {activity && } + {activity && } @@ -226,4 +238,4 @@ const ManagePublishedProjectCard = ({ ); }; -export default ManagePublishedProjectCard; +export default ManagePublishedArticle; diff --git a/client/src/modules/manage-projects/hooks/index.ts b/client/src/modules/settings/modules/manage-articles/v1/hooks/index.ts similarity index 57% rename from client/src/modules/manage-projects/hooks/index.ts rename to client/src/modules/settings/modules/manage-articles/v1/hooks/index.ts index 1f7f35e5..31f0ef13 100644 --- a/client/src/modules/manage-projects/hooks/index.ts +++ b/client/src/modules/settings/modules/manage-articles/v1/hooks/index.ts @@ -1,22 +1,22 @@ -import { useCallback } from 'react'; -import { useSetAtom, useAtomValue } from 'jotai'; +import { useCallback, useState } from 'react'; +import { useAtom } from 'jotai'; import { userProjects, userProjectsCount, -} from '../../../infra/rest/apis/project'; +} from '../../../../../../infra/rest/apis/project'; import { PublishedProjectsAtom, DraftProjectsAtom, ManageProjectsPaginationState, } from '../states'; -import { useAuth } from '../../../shared/hooks/use-auth'; -const useManageProjects = () => { - const setPublishedProjects = useSetAtom(PublishedProjectsAtom); - const setDraftProjects = useSetAtom(DraftProjectsAtom); - const publishedProjects = useAtomValue(PublishedProjectsAtom); - const draftProjects = useAtomValue(DraftProjectsAtom); - const { isAuthenticated } = useAuth(); +const useManageArticles = () => { + const [publishedArticles, setPublishedArticles] = useAtom( + PublishedProjectsAtom + ); + const [draftArticles, setDraftArticles] = useAtom(DraftProjectsAtom); + + const [query, setQuery] = useState(''); const fetchProjects = useCallback( async (params: { @@ -25,8 +25,6 @@ const useManageProjects = () => { query?: string; deletedDocCount?: number; }) => { - if (!isAuthenticated()) return; - const { page, is_draft, query = '', deletedDocCount = 0 } = params; try { @@ -40,7 +38,7 @@ const useManageProjects = () => { const newResults = projectsResponse.data || []; if (is_draft) { - setDraftProjects( + setDraftArticles( (prevState: ManageProjectsPaginationState | null) => { const previousResults = prevState?.results || []; @@ -58,7 +56,7 @@ const useManageProjects = () => { } ); } else { - setPublishedProjects( + setPublishedArticles( (prevState: ManageProjectsPaginationState | null) => { const previousResults = prevState?.results || []; @@ -81,14 +79,53 @@ const useManageProjects = () => { console.error('Error fetching projects:', error); } }, - [isAuthenticated, setPublishedProjects, setDraftProjects] + [setPublishedArticles, setDraftArticles] ); + const handleSearchChange = (value: string) => { + setQuery(value); + if (!value.length) { + setPublishedArticles(null); + setDraftArticles(null); + } + }; + + const handleSearchSubmit = () => { + if (query.length) { + setPublishedArticles(null); + setDraftArticles(null); + } + }; + + const handleSearchClear = () => { + setQuery(''); + setPublishedArticles(null); + setDraftArticles(null); + }; + + const handleLoadMore = (is_draft: boolean) => { + const currentState = is_draft ? draftArticles : publishedArticles; + if (currentState && currentState.results.length < currentState.totalDocs) { + fetchProjects({ + page: currentState.page + 1, + is_draft, + query, + deletedDocCount: currentState.deletedDocCount || 0, + }); + } + }; + return { fetchProjects, - publishedProjects, - draftProjects, + publishedArticles, + draftArticles, + query, + setQuery, + handleSearchChange, + handleSearchSubmit, + handleSearchClear, + handleLoadMore, }; }; -export default useManageProjects; +export default useManageArticles; diff --git a/client/src/modules/settings/modules/manage-articles/v1/index.tsx b/client/src/modules/settings/modules/manage-articles/v1/index.tsx new file mode 100644 index 00000000..c3c5f864 --- /dev/null +++ b/client/src/modules/settings/modules/manage-articles/v1/index.tsx @@ -0,0 +1,129 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import InPageNavigation from '../../../../../shared/components/molecules/page-navigation'; +import NoDataMessageBox from '../../../../../shared/components/atoms/no-data-msg'; +import ManagePublishedArticle from './components/publish-articles'; +import ManageDraftArticle from './components/draft-articles'; +import useManageArticles from './hooks'; +import { Box, CircularProgress } from '@mui/material'; +import Button from '../../../../../shared/components/atoms/button'; +import Header from '../../../../../shared/components/organisms/header'; + +const ManageArticles = () => { + const [searchParams] = useSearchParams(); + const activeTab = searchParams.get('tab'); + const { + fetchProjects, + publishedArticles, + draftArticles, + query, + handleSearchChange, + handleSearchSubmit, + handleSearchClear, + handleLoadMore, + } = useManageArticles(); + + useEffect(() => { + if (publishedArticles === null) { + fetchProjects({ page: 1, is_draft: false, query }); + } + if (draftArticles === null) { + fetchProjects({ page: 1, is_draft: true, query }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [publishedArticles, draftArticles, query]); + + return ( + <> +
+ + + + {/* Published Articles */} + {publishedArticles === null ? ( + + + + ) : publishedArticles.results.length ? ( + <> + {publishedArticles.results.map((project, i) => ( + + ))} + + {publishedArticles.results.length < + publishedArticles.totalDocs && ( + + + + )} + + ) : ( + + )} + + {/* Draft Articles */} + {draftArticles === null ? ( + + + + ) : draftArticles.results.length ? ( + <> + {draftArticles.results.map((project, i) => ( + + ))} + + {draftArticles.results.length < draftArticles.totalDocs && ( + + + + )} + + ) : ( + + )} + + + + ); +}; + +export default ManageArticles; diff --git a/client/src/modules/manage-projects/states/index.ts b/client/src/modules/settings/modules/manage-articles/v1/states/index.ts similarity index 80% rename from client/src/modules/manage-projects/states/index.ts rename to client/src/modules/settings/modules/manage-articles/v1/states/index.ts index 4338a699..e0859f82 100644 --- a/client/src/modules/manage-projects/states/index.ts +++ b/client/src/modules/settings/modules/manage-articles/v1/states/index.ts @@ -1,5 +1,5 @@ import { atom } from 'jotai'; -import { userProjectsResponse } from '../../../infra/rest/apis/project/typing'; +import { userProjectsResponse } from '../../../../../../infra/rest/apis/project/typing'; export interface ManageProjectsPaginationState { results: userProjectsResponse[]; diff --git a/client/src/modules/settings/routes/index.tsx b/client/src/modules/settings/routes/index.tsx index a23fa64d..b71b59e1 100644 --- a/client/src/modules/settings/routes/index.tsx +++ b/client/src/modules/settings/routes/index.tsx @@ -1,5 +1,6 @@ import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined'; import ExtensionIcon from '@mui/icons-material/Extension'; +import ArticleIcon from '@mui/icons-material/Article'; import { ROUTES_V1, ROUTES_SETTINGS_V1, @@ -12,6 +13,7 @@ import { ProfileLazyComponentV1, IntegrationsLazyComponentV1, OpenAILazyComponentV1, + ManageArticlesLazyComponentV1, } from '../modules'; import OpenAIIcon from '../../../shared/icons/openai'; @@ -28,6 +30,13 @@ export const settingsRoutes = ({ name: 'Your profile', description: 'Edit your personal details', }, + { + id: 'manage-articles', + icon: , + path: ROUTES_SETTINGS_V1.MANAGE_ARTICLES, + name: 'Manage Articles', + description: 'Manage your articles', + }, { id: 'integrations', icon: , @@ -45,6 +54,16 @@ export const settingsRoutes = ({ } />, + + } + />, = ({ open={isMobileMenuOpen} onClose={handleMobileMenuClose} > - {actions.map(action => ( - handleActionClick(action)}> - {action.icon} - - - ))} + {actions.map(action => { + const mobileIcon = action.mobileIcon ?? action.icon; + return ( + handleActionClick(action)}> + {mobileIcon && {mobileIcon}} + + + ); + })} ); }; diff --git a/client/src/shared/components/organisms/header/index.tsx b/client/src/shared/components/organisms/header/index.tsx index a92b4b04..a246d219 100644 --- a/client/src/shared/components/organisms/header/index.tsx +++ b/client/src/shared/components/organisms/header/index.tsx @@ -47,7 +47,14 @@ const Header = ({ const hasRightActions = Boolean(rightSideActions?.length); return ( - + theme.zIndex.appBar, + bgcolor: 'background.paper', + }} + > @@ -93,16 +99,21 @@ const Header = ({ {hasRightActions && ( {rightSideActions?.map(action => ( - - {action.icon} - + + {action.desktopNode ? ( + action.desktopNode + ) : ( + + {action.icon} + + )} + ))} )} diff --git a/client/src/shared/components/organisms/header/typings/index.ts b/client/src/shared/components/organisms/header/typings/index.ts index 98ae5d50..85127a10 100644 --- a/client/src/shared/components/organisms/header/typings/index.ts +++ b/client/src/shared/components/organisms/header/typings/index.ts @@ -4,6 +4,8 @@ export interface HeaderAction { key: string; label: string; icon: ReactNode; + desktopNode?: ReactNode; + mobileIcon?: ReactNode; link?: string; onClick?: () => void; ariaLabel?: string; diff --git a/client/src/shared/components/organisms/sidebar/index.tsx b/client/src/shared/components/organisms/sidebar/index.tsx index 784a0406..dca2e209 100644 --- a/client/src/shared/components/organisms/sidebar/index.tsx +++ b/client/src/shared/components/organisms/sidebar/index.tsx @@ -29,7 +29,7 @@ const Sidebar = () => { position: 'absolute', height: '100%', top: 0, - zIndex: 3, + zIndex: theme => theme.zIndex.drawer + 1, left: 0, width: showExpandedView ? '230px' : `${SIDEBAR_WIDTH}px`, minWidth: showExpandedView ? '230px' : `${SIDEBAR_WIDTH}px`, diff --git a/client/src/shared/constants/index.ts b/client/src/shared/constants/index.ts index 718f14d0..91913618 100644 --- a/client/src/shared/constants/index.ts +++ b/client/src/shared/constants/index.ts @@ -1 +1,7 @@ export const PAGE_SIZE = 10; + +export const defaultLightThumbnail = + 'https://res.cloudinary.com/avdhesh-varshney/image/upload/v1761476229/OS7sL4eIr_MiI9fqvkYCw-1761476227343.jpg'; + +export const defaultDarkThumbnail = + 'https://res.cloudinary.com/avdhesh-varshney/image/upload/v1761476020/FHX9kToS3GiG0sgUFQ_Mo-1761476018134.jpg';