mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-16 09:41:31 +10:00
🚀 release v3.0.0
This commit is contained in:
75
client/pages/[username]/[slug]/build.tsx
Normal file
75
client/pages/[username]/[slug]/build.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import Center from '@/components/build/Center/Center';
|
||||
import LeftSidebar from '@/components/build/LeftSidebar/LeftSidebar';
|
||||
import RightSidebar from '@/components/build/RightSidebar/RightSidebar';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import { setResume } from '@/store/resume/resumeSlice';
|
||||
import styles from '@/styles/pages/Build.module.scss';
|
||||
|
||||
type QueryParams = {
|
||||
username: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async ({ query, locale = 'en' }) => {
|
||||
const { username, slug } = query as QueryParams;
|
||||
|
||||
return {
|
||||
props: { username, slug, ...(await serverSideTranslations(locale, ['common', 'modals', 'builder'])) },
|
||||
};
|
||||
};
|
||||
|
||||
const Build: NextPage<Props> = ({ username, slug }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data: resume } = useQuery<Resume>(
|
||||
`resume/${username}/${slug}`,
|
||||
() => fetchResumeByIdentifier({ username, slug }),
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (resume) => {
|
||||
dispatch(setResume(resume));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (resume) dispatch(setResume(resume));
|
||||
}, [resume, dispatch]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>
|
||||
{resume.name} | {t('common.title')}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
<LeftSidebar />
|
||||
<Center />
|
||||
<RightSidebar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Build;
|
||||
128
client/pages/[username]/[slug]/index.tsx
Normal file
128
client/pages/[username]/[slug]/index.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setResume } from '@/store/resume/resumeSlice';
|
||||
import styles from '@/styles/pages/Preview.module.scss';
|
||||
|
||||
type QueryParams = {
|
||||
slug: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
slug: string;
|
||||
resume?: Resume;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async ({ query, locale = 'en' }) => {
|
||||
const { username, slug } = query as QueryParams;
|
||||
|
||||
try {
|
||||
const resume = await fetchResumeByIdentifier({ username, slug });
|
||||
|
||||
return {
|
||||
props: { username, slug, resume, ...(await serverSideTranslations(locale, ['common'])) },
|
||||
};
|
||||
} catch {
|
||||
return { props: { username, slug, ...(await serverSideTranslations(locale, ['common'])) } };
|
||||
}
|
||||
};
|
||||
|
||||
const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && !isEmpty(initialData)) {
|
||||
dispatch(setResume(initialData));
|
||||
}
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useQuery<Resume>(`resume/${username}/${slug}`, () => fetchResumeByIdentifier({ username, slug }), {
|
||||
initialData,
|
||||
retry: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (data) => {
|
||||
dispatch(setResume(data));
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorObj = JSON.parse(JSON.stringify(error));
|
||||
const statusCode: number = get(errorObj, 'status', 404);
|
||||
|
||||
if (statusCode === 404) {
|
||||
toast.error('The resume you were looking for does not exist, or maybe it never did?');
|
||||
|
||||
router.push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
if (isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username, slug });
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('preview-mode', styles.container)}>
|
||||
{layout.map((_, pageIndex) => (
|
||||
<Page key={pageIndex} page={pageIndex} />
|
||||
))}
|
||||
|
||||
<div className={clsx(styles.download, { 'opacity-75': isLoading })}>
|
||||
<ButtonBase onClick={handleDownload} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Downloading />
|
||||
<h4>Please wait</h4>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download />
|
||||
<h4>Download PDF</h4>
|
||||
</>
|
||||
)}
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
<p className={styles.footer}>
|
||||
Made with <Link href="/">Reactive Resume</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
66
client/pages/[username]/[slug]/printer.tsx
Normal file
66
client/pages/[username]/[slug]/printer.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setResume } from '@/store/resume/resumeSlice';
|
||||
import styles from '@/styles/pages/Printer.module.scss';
|
||||
|
||||
type QueryParams = {
|
||||
slug: string;
|
||||
username: string;
|
||||
secretKey?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
resume?: Resume;
|
||||
redirect?: any;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({ query }) => {
|
||||
const { username, slug, secretKey } = query as QueryParams;
|
||||
|
||||
try {
|
||||
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
|
||||
|
||||
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
||||
|
||||
return { props: { resume } };
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: '/',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Printer: NextPage<Props> = ({ resume: initialData }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) dispatch(setResume(initialData));
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
return (
|
||||
<div className={clsx('printer-mode', styles.container)}>
|
||||
{layout.map((_, pageIndex) => (
|
||||
<Page key={pageIndex} page={pageIndex} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Printer;
|
||||
59
client/pages/_app.tsx
Normal file
59
client/pages/_app.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import '@/styles/globals.scss';
|
||||
|
||||
import DateAdapter from '@mui/lab/AdapterDayjs';
|
||||
import LocalizationProvider from '@mui/lab/LocalizationProvider';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
|
||||
import Loading from '@/components/shared/Loading';
|
||||
import ModalWrapper from '@/modals/index';
|
||||
import queryClient from '@/services/react-query';
|
||||
import store, { persistor } from '@/store/index';
|
||||
import WrapperRegistry from '@/wrappers/index';
|
||||
|
||||
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reactive Resume</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
|
||||
/>
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
</Head>
|
||||
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={DateAdapter}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
|
||||
<Component {...pageProps} />
|
||||
|
||||
<ModalWrapper />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
className: 'toast',
|
||||
}}
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
</PersistGate>
|
||||
</LocalizationProvider>
|
||||
</ReduxProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default appWithTranslation(App);
|
||||
73
client/pages/dashboard.tsx
Normal file
73
client/pages/dashboard.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Add, ImportExport } from '@mui/icons-material';
|
||||
import type { GetStaticProps, NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import ResumeCard from '@/components/dashboard/ResumeCard';
|
||||
import ResumePreview from '@/components/dashboard/ResumePreview';
|
||||
import Avatar from '@/components/shared/Avatar';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { fetchResumes } from '@/services/resume';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data } = useQuery(RESUMES_QUERY, fetchResumes);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>
|
||||
{t('dashboard.title')} | {t('common.title')}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Avatar size={40} />
|
||||
</header>
|
||||
|
||||
<main className={styles.resumes}>
|
||||
<ResumeCard
|
||||
modal="dashboard.create-resume"
|
||||
icon={Add}
|
||||
title={t('dashboard.create-resume.title')}
|
||||
subtitle={t('dashboard.create-resume.subtitle')}
|
||||
/>
|
||||
|
||||
<ResumeCard
|
||||
modal="dashboard.import-external"
|
||||
icon={ImportExport}
|
||||
title={t('dashboard.import-external.title')}
|
||||
subtitle={t('dashboard.import-external.subtitle')}
|
||||
/>
|
||||
|
||||
{data.map((resume) => (
|
||||
<ResumePreview key={resume.id} resume={resume} />
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
145
client/pages/index.tsx
Normal file
145
client/pages/index.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { Link as LinkIcon } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import type { GetStaticProps, NextPage } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import Footer from '@/components/shared/Footer';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import NoSSR from '@/components/shared/NoSSR';
|
||||
import { screenshots } from '@/config/screenshots';
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DONATION_URL, GITHUB_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
|
||||
|
||||
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
|
||||
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } }));
|
||||
|
||||
const handleLogout = () => dispatch(logout());
|
||||
|
||||
return (
|
||||
<main className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logo}>
|
||||
<Logo size={256} />
|
||||
</div>
|
||||
|
||||
<div className={styles.main}>
|
||||
<h1>{t('common.title')}</h1>
|
||||
|
||||
<h2>{t('common.subtitle')}</h2>
|
||||
|
||||
<NoSSR>
|
||||
<div className={styles.buttonWrapper}>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Link href="/dashboard" passHref>
|
||||
<Button>{t('landing.actions.app')}</Button>
|
||||
</Link>
|
||||
|
||||
<Button variant="outlined" onClick={handleLogout}>
|
||||
{t('landing.actions.logout')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={handleLogin}>{t('landing.actions.login')}</Button>
|
||||
|
||||
<Button variant="outlined" onClick={handleRegister}>
|
||||
{t('landing.actions.register')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</NoSSR>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.summary.heading')}</h6>
|
||||
|
||||
<p>{t('landing.summary.body')}</p>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.features.heading')}</h6>
|
||||
|
||||
<ul className="list-inside list-disc leading-loose">
|
||||
<li>{t('landing.features.list.free')}</li>
|
||||
<li>{t('landing.features.list.ads')}</li>
|
||||
<li>{t('landing.features.list.tracking')}</li>
|
||||
<li>{t('landing.features.list.languages')}</li>
|
||||
<li>{t('landing.features.list.import')}</li>
|
||||
<li>{t('landing.features.list.export')}</li>
|
||||
<li>
|
||||
<Trans t={t} i18nKey="landing.features.list.more">
|
||||
And a lot of exciting features,
|
||||
<a href={`${GITHUB_URL}#features`} target="_blank" rel="noreferrer">
|
||||
click here to know more
|
||||
</a>
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.screenshots.heading')}</h6>
|
||||
|
||||
<div className={styles.screenshots}>
|
||||
{screenshots.map(({ src, alt }) => (
|
||||
<a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer">
|
||||
<Image src={src} alt={alt} layout="fill" objectFit="cover" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.links.heading')}</h6>
|
||||
|
||||
<div>
|
||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.github')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.donate')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<Footer className="font-semibold leading-5 opacity-50" />
|
||||
|
||||
<div>v{process.env.appVersion}</div>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
110
client/pages/r/[shortId].tsx
Normal file
110
client/pages/r/[shortId].tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
import { fetchResumeByShortId } from '@/services/resume';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setResume } from '@/store/resume/resumeSlice';
|
||||
import styles from '@/styles/pages/Preview.module.scss';
|
||||
|
||||
type QueryParams = {
|
||||
shortId: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
shortId: string;
|
||||
resume?: Resume;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async ({ query, locale = 'en' }) => {
|
||||
const { shortId } = query as QueryParams;
|
||||
|
||||
try {
|
||||
const resume = await fetchResumeByShortId({ shortId });
|
||||
|
||||
return { props: { shortId, resume, ...(await serverSideTranslations(locale, ['common'])) } };
|
||||
} catch {
|
||||
return { props: { shortId, ...(await serverSideTranslations(locale, ['common'])) } };
|
||||
}
|
||||
};
|
||||
|
||||
const Preview: NextPage<Props> = ({ shortId, resume: initialData }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && !isEmpty(initialData)) {
|
||||
dispatch(setResume(initialData));
|
||||
}
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useQuery<Resume>(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), {
|
||||
initialData,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (data) => {
|
||||
dispatch(setResume(data));
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
if (isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username: resume.user.username, slug: resume.slug });
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('preview-mode', styles.container)}>
|
||||
{layout.map((_, pageIndex) => (
|
||||
<Page key={pageIndex} page={pageIndex} />
|
||||
))}
|
||||
|
||||
<div className={clsx(styles.download, { 'opacity-75': isLoading })}>
|
||||
<ButtonBase onClick={handleDownload} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Downloading />
|
||||
<h4>Please wait</h4>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download />
|
||||
<h4>Download PDF</h4>
|
||||
</>
|
||||
)}
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
<p className={styles.footer}>
|
||||
Made with <Link href="/">Reactive Resume</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
Reference in New Issue
Block a user