🚀 release v3.0.0

This commit is contained in:
Amruth Pillai
2022-03-06 22:48:29 +01:00
parent 00505a9e5d
commit 9c1380f401
373 changed files with 12050 additions and 15783 deletions

View 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;

View 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;

View 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
View 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);

View 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
View 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;

View 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;