Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 52 additions & 35 deletions client/src/modules/manage-projects/components/draft-projects.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,65 @@
import { Link } from 'react-router-dom';
import {
Box,
Typography,
Button,
Card,
CardContent,
Stack,
} from '@mui/material';
import { Project, deleteProject } from './utils';

const ManageDraftProjectPost = ({ project }: { project: Project }) => {
const { title, des } = project;
let { index = 0 } = project;

index++;

return (
<div className="flex gap-10 border-b mb-6 max-md:px-4 border-gray-100 pb-6 items-center">
<h1 className="blog-index text-center pl-4 md:pl-6 flex-none">
{index < 10 ? '0' + index : index}
</h1>

<div>
<h1 className="blog-title mb-3">{title}</h1>

<p className="line-clamp-2 font-gelasio">
{des?.length ? des : 'No Description'}
</p>
<Card variant="outlined" sx={{ mb: 2, borderRadius: 2 }}>
<CardContent>
<Stack
direction={{ xs: 'column', md: 'row' }}
alignItems="center"
spacing={2}
>
<Box sx={{ minWidth: 56, textAlign: 'center' }}>
<Typography sx={{ fontWeight: 600 }}>
{index < 10 ? '0' + index : index}
</Typography>
</Box>

<div className="flex gap-6 mt-3">
<Link
to={`/editor/${project.project_id}`}
className="pr-4 py-2 underline"
>
Edit
</Link>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{title}
</Typography>

<button
className="lg:hidden pr-4 py-2 underline text-red"
onClick={e => deleteProject(project, '', e.target as HTMLElement)}
>
Delete
</button>
</div>
</div>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{des?.length ? des : 'No Description'}
</Typography>

<div className="max-lg:hidden">
<button
className="lg:block hidden pr-4 py-2 underline text-red"
onClick={e => deleteProject(project, '', e.target as HTMLElement)}
>
Delete
</button>
</div>
</div>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
component={Link}
to={`/editor/${project.project_id}`}
size="small"
>
Edit
</Button>
<Button
size="small"
color="error"
onClick={e =>
deleteProject(project, '', e.target as HTMLElement)
}
>
Delete
</Button>
</Box>
</Box>
</Stack>
</CardContent>
</Card>
);
};

Expand Down
255 changes: 114 additions & 141 deletions client/src/modules/manage-projects/components/publish-projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ 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 {
Box,
Typography,
Button,
Card,
CardContent,
CardActions,
Avatar,
Stack,
} from '@mui/material';
import { Project, deleteProject } from './utils';

interface ProjectStats {
total_likes: number;
Expand All @@ -15,104 +22,42 @@ 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<AllProjectsData | null>) => 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 (
<div className="flex gap-2 max-lg:mb-6 max-lg:pb-6 border-gray-100 max-lg:border-b">
{Object.keys(stats).map((key, i) => {
return !key.includes('parent') ? (
<div
<Stack
direction={{ xs: 'column', md: 'row' }}
sx={{ borderTop: { xs: '1px solid #e5e7eb', md: 'none' } }}
>
{Object.keys(stats).map((key, i) =>
!key.includes('parent') ? (
<Box
key={i}
className={
'flex flex-col items-center w-full h-full justify-center p-4 px-6 ' +
(i !== 0 ? ' border-gray-100 border-l' : '')
}
sx={{
p: 2,
px: 3,
borderLeft: i !== 0 ? '1px solid #e5e7eb' : 'none',
textAlign: 'center',
}}
>
<h1 className="text-xl lg:text-2xl mb-2">
{stats[key].toLocaleString()}
</h1>
<p className="max-lg:text-gray-500 capitalize">
<Typography variant="h6">{stats[key].toLocaleString()}</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ textTransform: 'capitalize' }}
>
{key.split('_')[1]}
</p>
</div>
) : (
''
);
})}
</div>
</Typography>
</Box>
) : null
)}
</Stack>
);
};

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;
Expand All @@ -123,60 +68,88 @@ const ManagePublishedProjectCard = ({ project }: { project: Project }) => {
const [showStat, setShowStat] = useState(false);

return (
<>
<div className="flex gap-10 border-b mb-6 max-md:px-4 border-gray-100 pb-6 items-center">
<img
src={banner}
alt=""
className="max-md:hidden lg:hidden xl:block w-28 h-28 flex-none bg-gray-100 object-cover"
/>

<div className="flex flex-col justify-between py-2 w-full min-w-[300px]">
<div>
<Link
<Card variant="outlined" sx={{ mb: 2, borderRadius: 2 }}>
<CardContent>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={2}
alignItems="center"
>
{banner ? (
<Box
component="img"
src={banner}
alt=""
sx={{
width: 112,
height: 112,
objectFit: 'cover',
borderRadius: 1,
display: { xs: 'none', xl: 'block' },
}}
/>
) : (
<Avatar
variant="rounded"
sx={{
width: 112,
height: 112,
display: { xs: 'none', xl: 'block' },
}}
/>
)}

<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
component={Link}
to={`/project/${project_id}`}
className="project-title mb-4 hover:underline"
sx={{
display: 'block',
fontWeight: 600,
textDecoration: 'none',
color: 'text.primary',
mb: 1,
}}
>
{title}
</Link>

<p className="line-clamp-1">Published on {getDay(publishedAt)}</p>
</div>

<div className="flex gap-6 mt-3">
<Link to={`/editor/${project_id}`} className="pr-4 py-2 underline">
Edit
</Link>

<button
className="lg:hidden pr-4 py-2 underline"
onClick={() => setShowStat(preVal => !preVal)}
>
Stats
</button>

<button
className="pr-4 py-2 underline text-red-500"
onClick={e => deleteProject(project, access_token, e.target)}
>
Delete
</button>
</div>
</div>

<div className="max-lg:hidden">
{activity && <ProjectStats stats={activity} />}
</div>
</div>
</Typography>
<Typography variant="body2" color="text.secondary">
Published on {getDay(publishedAt)}
</Typography>

<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
<Button
component={Link}
to={`/editor/${project_id}`}
size="small"
>
Edit
</Button>
<Button size="small" onClick={() => setShowStat(pre => !pre)}>
Stats
</Button>
<Button
size="small"
color="error"
onClick={e => deleteProject(project, access_token, e.target)}
>
Delete
</Button>
</Box>
</Box>

<Box sx={{ display: { xs: 'none', md: 'block' } }}>
{activity && <ProjectStats stats={activity} />}
</Box>
</Stack>
</CardContent>

{showStat ? (
<div className="lg:hidden">
<CardActions>
{activity && <ProjectStats stats={activity} />}
</div>
) : (
''
)}
</>
</CardActions>
) : null}
</Card>
);
};

Expand Down
Loading
Loading