mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 08:13:49 +10:00
🚀 release v3.0.0
This commit is contained in:
24
client/components/dashboard/ResumeCard.module.scss
Normal file
24
client/components/dashboard/ResumeCard.module.scss
Normal file
@ -0,0 +1,24 @@
|
||||
.resume {
|
||||
@apply flex flex-col gap-2;
|
||||
|
||||
.preview {
|
||||
aspect-ratio: 1 / 1.41;
|
||||
|
||||
@apply flex items-center justify-center shadow;
|
||||
@apply cursor-pointer rounded-sm bg-neutral-100 transition-opacity hover:opacity-80 dark:bg-neutral-800;
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply flex items-center justify-between;
|
||||
|
||||
.meta {
|
||||
p:first-child {
|
||||
@apply text-sm font-semibold leading-relaxed;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
@apply text-xs leading-relaxed opacity-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
client/components/dashboard/ResumeCard.tsx
Normal file
39
client/components/dashboard/ResumeCard.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { SvgIconComponent } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import { ModalName, setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
import styles from './ResumeCard.module.scss';
|
||||
|
||||
type Props = {
|
||||
modal: ModalName;
|
||||
icon: SvgIconComponent;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
|
||||
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(setModalState({ modal, state: { open: true } }));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.resume}>
|
||||
<ButtonBase className={styles.preview} onClick={handleClick}>
|
||||
<Icon sx={{ fontSize: 64 }} />
|
||||
</ButtonBase>
|
||||
|
||||
<footer>
|
||||
<div className={styles.meta}>
|
||||
<p>{title}</p>
|
||||
<p>{subtitle}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumeCard;
|
||||
37
client/components/dashboard/ResumePreview.module.scss
Normal file
37
client/components/dashboard/ResumePreview.module.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.resume {
|
||||
@apply flex flex-col gap-2;
|
||||
|
||||
.preview {
|
||||
aspect-ratio: 1 / 1.41;
|
||||
|
||||
@apply relative cursor-pointer rounded-sm shadow;
|
||||
@apply bg-neutral-100 transition-opacity hover:opacity-80 dark:bg-neutral-800;
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply flex items-center justify-between overflow-hidden;
|
||||
|
||||
.meta {
|
||||
flex: 4;
|
||||
@apply flex flex-col overflow-hidden;
|
||||
|
||||
p {
|
||||
@apply overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
|
||||
&:first-child {
|
||||
@apply text-sm font-semibold leading-relaxed;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply text-xs leading-relaxed opacity-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex: 1;
|
||||
|
||||
@apply h-full w-full cursor-pointer rounded text-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
190
client/components/dashboard/ResumePreview.tsx
Normal file
190
client/components/dashboard/ResumePreview.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import {
|
||||
ContentCopy,
|
||||
DeleteOutline,
|
||||
DriveFileRenameOutline,
|
||||
Link as LinkIcon,
|
||||
MoreVert,
|
||||
OpenInNew,
|
||||
} from '@mui/icons-material';
|
||||
import { ButtonBase, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import queryClient from '@/services/react-query';
|
||||
import { deleteResume, DeleteResumeParams, duplicateResume, DuplicateResumeParams } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import { getRelativeTime } from '@/utils/date';
|
||||
import getResumeUrl from '@/utils/getResumeUrl';
|
||||
|
||||
import styles from './ResumePreview.module.scss';
|
||||
|
||||
type Props = {
|
||||
resume: Resume;
|
||||
};
|
||||
|
||||
const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const handleOpen = () => {
|
||||
handleClose();
|
||||
|
||||
router.push({
|
||||
pathname: '/[username]/[slug]/build',
|
||||
query: { username: resume.user.username, slug: resume.slug },
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenMenu = (event: React.MouseEvent<Element>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleRename = () => {
|
||||
handleClose();
|
||||
|
||||
dispatch(
|
||||
setModalState({
|
||||
modal: 'dashboard.rename-resume',
|
||||
state: {
|
||||
open: true,
|
||||
payload: {
|
||||
item: resume,
|
||||
onComplete: () => {
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
handleClose();
|
||||
|
||||
await duplicateMutation({ id: resume.id });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
};
|
||||
|
||||
const handleShareLink = async () => {
|
||||
handleClose();
|
||||
|
||||
const url = getResumeUrl(resume, { withHost: true });
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
toast.success(t('common.toast.success.resume-link-copied'));
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
handleClose();
|
||||
|
||||
await deleteMutation({ id: resume.id });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.resume}>
|
||||
<Link
|
||||
passHref
|
||||
href={{
|
||||
pathname: '/[username]/[slug]/build',
|
||||
query: { username: resume.user.username, slug: resume.slug },
|
||||
}}
|
||||
>
|
||||
<ButtonBase className={styles.preview}>
|
||||
{resume.image ? (
|
||||
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
|
||||
) : null}
|
||||
</ButtonBase>
|
||||
</Link>
|
||||
|
||||
<footer>
|
||||
<div className={styles.meta}>
|
||||
<p>{resume.name}</p>
|
||||
<p>{t('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
||||
</div>
|
||||
|
||||
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
||||
<MoreVert />
|
||||
</ButtonBase>
|
||||
|
||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||
<MenuItem onClick={handleOpen}>
|
||||
<ListItemIcon>
|
||||
<OpenInNew className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.open')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleRename}>
|
||||
<ListItemIcon>
|
||||
<DriveFileRenameOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.rename')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleDuplicate}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{resume.public ? (
|
||||
<MenuItem onClick={handleShareLink}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.share-link')}>
|
||||
<div>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.delete')}>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<ListItemIcon>
|
||||
<DeleteOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumePreview;
|
||||
Reference in New Issue
Block a user