release: v3.6.9

This commit is contained in:
Amruth Pillai
2022-11-13 14:28:47 +01:00
parent 89b35392bd
commit 8026241b6c
59 changed files with 1600 additions and 1527 deletions

View File

@ -46,6 +46,6 @@ EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \ HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
CMD curl -fSs 127.0.0.1:3000 || exit 1 CMD curl -fSs localhost:3000 || exit 1
CMD [ "pnpm", "run", "start", "--filter", "client" ] CMD [ "pnpm", "run", "start", "--filter", "client" ]

View File

@ -26,8 +26,8 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
const theme: ThemeConfig = get(resume, 'metadata.theme'); const theme: ThemeConfig = get(resume, 'metadata.theme');
const customCSS: CustomCSS = get(resume, 'metadata.css'); const customCSS: CustomCSS = get(resume, 'metadata.css');
const template: string = get(resume, 'metadata.template'); const template: string = get(resume, 'metadata.template');
const pageConfig: PageConfig = get(resume, 'metadata.page');
const typography: Typography = get(resume, 'metadata.typography'); const typography: Typography = get(resume, 'metadata.typography');
const pageConfig: PageConfig = get(resume, 'metadata.page', {} as PageConfig);
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]); const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]); const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);

View File

@ -111,9 +111,7 @@ const LeftSidebar = () => {
<nav className="overflow-y-scroll"> <nav className="overflow-y-scroll">
<div> <div>
<Link href="/dashboard"> <Link href="/dashboard">
<a className="inline-flex">
<Logo size={40} /> <Logo size={40} />
</a>
</Link> </Link>
<Divider /> <Divider />
</div> </div>
@ -132,7 +130,7 @@ const LeftSidebar = () => {
{customSections.map(({ id }) => ( {customSections.map(({ id }) => (
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow> <Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
<IconButton onClick={() => handleClick(id)}> <IconButton onClick={() => id && handleClick(id)}>
<Star /> <Star />
</IconButton> </IconButton>
</Tooltip> </Tooltip>

View File

@ -79,7 +79,7 @@ const Section: React.FC<Props> = ({
visible: true, visible: true,
columns: 2, columns: 2,
items: [], items: [],
isDuplicated: true isDuplicated: true,
}; };
dispatch(duplicateSection({ value: newSection, type })); dispatch(duplicateSection({ value: newSection, type }));

View File

@ -17,7 +17,9 @@ const CustomCSS = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {})); const customCSS: CustomCSSType = useAppSelector((state) =>
get(state.resume.present, 'metadata.css', {} as CustomCSSType)
);
const handleChange = (value: string | undefined) => { const handleChange = (value: string | undefined) => {
dispatch(setResumeState({ path: 'metadata.css.value', value })); dispatch(setResumeState({ path: 'metadata.css.value', value }));

View File

@ -47,8 +47,8 @@ const Settings = () => {
const id: number = useMemo(() => get(resume, 'id'), [resume]); const id: number = useMemo(() => get(resume, 'id'), [resume]);
const slug: string = useMemo(() => get(resume, 'slug'), [resume]); const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
const username: string = useMemo(() => get(resume, 'user.username'), [resume]); const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
const pageConfig: PageConfig = useMemo(() => get(resume, 'metadata.page'), [resume]);
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]); const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
const pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
const isDarkMode = useMemo(() => theme === 'dark', [theme]); const isDarkMode = useMemo(() => theme === 'dark', [theme]);
const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]); const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
@ -98,7 +98,7 @@ const Settings = () => {
<> <>
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} /> <Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
<List sx={{ padding: 0 }}> <List disablePadding>
{/* Global Settings */} {/* Global Settings */}
<> <>
<ListSubheader disableSticky className="rounded"> <ListSubheader disableSticky className="rounded">
@ -212,7 +212,7 @@ const Settings = () => {
{t<string>('builder.rightSidebar.sections.settings.resume.heading')} {t<string>('builder.rightSidebar.sections.settings.resume.heading')}
</ListSubheader> </ListSubheader>
<ListItem> <ListItem disableGutters>
<ListItemButton onClick={handleLoadSampleData}> <ListItemButton onClick={handleLoadSampleData}>
<ListItemIcon> <ListItemIcon>
<Anchor /> <Anchor />
@ -224,7 +224,7 @@ const Settings = () => {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem> <ListItem disableGutters>
<ListItemButton onClick={handleResetResume}> <ListItemButton onClick={handleResetResume}>
<ListItemIcon> <ListItemIcon>
<DeleteForever /> <DeleteForever />

View File

@ -31,7 +31,14 @@ const Templates = () => {
<div key={template.id} className={styles.template}> <div key={template.id} className={styles.template}>
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}> <div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
<ButtonBase onClick={() => handleChange(template)}> <ButtonBase onClick={() => handleChange(template)}>
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority /> <Image
fill
priority
alt={template.name}
src={template.preview}
className="rounded-sm"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</ButtonBase> </ButtonBase>
</div> </div>

View File

@ -16,9 +16,7 @@ type Props = {
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => { const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleClick = () => { const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
dispatch(setModalState({ modal, state: { open: true } }));
};
return ( return (
<section className={styles.resume}> <section className={styles.resume}>

View File

@ -115,9 +115,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
}} }}
> >
<ButtonBase className={styles.preview}> <ButtonBase className={styles.preview}>
{resume.image ? ( {resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
) : null}
</ButtonBase> </ButtonBase>
</Link> </Link>

View File

@ -47,9 +47,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
<Image <Image
width={size} width={size}
height={size} height={size}
alt={user?.name}
className={styles.avatar} className={styles.avatar}
src={getGravatarUrl(email, size)} src={getGravatarUrl(email, size)}
alt={user?.name ?? 'User Avatar'}
/> />
</IconButton> </IconButton>

View File

@ -4,8 +4,8 @@ type Props = {
size?: 256 | 64 | 48 | 40 | 32; size?: 256 | 64 | 48 | 40 | 32;
}; };
const Logo: React.FC<Props> = ({ size = 64 }) => { const Logo: React.FC<Props> = ({ size = 64 }) => (
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />; <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
}; );
export default Logo; export default Logo;

View File

@ -75,54 +75,121 @@ export const left: SidebarSection[] = [
{ {
id: 'education', id: 'education',
icon: <School />, icon: <School />,
component: <Section type={"education"} path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />, component: (
<Section
type={'education'}
path="sections.education"
titleKey="institution"
subtitleKey="area"
isEditable
isHideable
/>
),
}, },
{ {
id: 'awards', id: 'awards',
icon: <EmojiEvents />, icon: <EmojiEvents />,
component: <Section type={"awards"} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />, component: (
<Section type={'awards'} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />
),
}, },
{ {
id: 'certifications', id: 'certifications',
icon: <CardGiftcard />, icon: <CardGiftcard />,
component: <Section type={"certifications"} path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />, component: (
<Section
type={'certifications'}
path="sections.certifications"
titleKey="name"
subtitleKey="issuer"
isEditable
isHideable
/>
),
}, },
{ {
id: 'publications', id: 'publications',
icon: <MenuBook />, icon: <MenuBook />,
component: <Section type={"publications"} path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />, component: (
<Section
type={'publications'}
path="sections.publications"
titleKey="name"
subtitleKey="publisher"
isEditable
isHideable
/>
),
}, },
{ {
id: 'skills', id: 'skills',
icon: <Architecture />, icon: <Architecture />,
component: <Section type={"skills"} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />, component: (
<Section type={'skills'} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />
),
}, },
{ {
id: 'languages', id: 'languages',
icon: <Language />, icon: <Language />,
component: <Section type={"languages"} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />, component: (
<Section type={'languages'} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />
),
}, },
{ {
id: 'interests', id: 'interests',
icon: <Sailing />, icon: <Sailing />,
component: <Section type={"interests"} path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />, component: (
<Section
type={'interests'}
path="sections.interests"
titleKey="name"
subtitleKey="keywords"
isEditable
isHideable
/>
),
}, },
{ {
id: 'volunteer', id: 'volunteer',
icon: <VolunteerActivism />, icon: <VolunteerActivism />,
component: ( component: (
<Section type={"volunteer"} path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable /> <Section
type={'volunteer'}
path="sections.volunteer"
titleKey="organization"
subtitleKey="position"
isEditable
isHideable
/>
), ),
}, },
{ {
id: 'projects', id: 'projects',
icon: <Coffee />, icon: <Coffee />,
component: <Section type={"projects"} path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />, component: (
<Section
type={'projects'}
path="sections.projects"
titleKey="name"
subtitleKey="description"
isEditable
isHideable
/>
),
}, },
{ {
id: 'references', id: 'references',
icon: <Groups />, icon: <Groups />,
component: <Section type={"references"} path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />, component: (
<Section
type={'references'}
path="sections.references"
titleKey="name"
subtitleKey="relationship"
isEditable
isHideable
/>
),
}, },
]; ];
@ -174,10 +241,7 @@ export const right: SidebarSection[] = [
}, },
]; ];
export const getSectionsByType = ( export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
sections: Record<string, SectionRecord>,
type: SectionType
): Array<Required<SectionRecord>> => {
if (isEmpty(sections)) return []; if (isEmpty(sections)) return [];
return Object.entries(sections).reduce((acc, [id, section]) => { return Object.entries(sections).reduce((acc, [id, section]) => {
@ -186,10 +250,10 @@ export const getSectionsByType = (
} }
return acc; return acc;
}, [] as Array<Required<SectionRecord>>); }, [] as SectionRecord[]);
}; };
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => { export const getCustomSections = (sections: Record<string, SectionRecord>): SectionRecord[] => {
if (isEmpty(sections)) return []; if (isEmpty(sections)) return [];
return Object.entries(sections).reduce((acc, [id, section]) => { return Object.entries(sections).reduce((acc, [id, section]) => {
@ -198,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
} }
return acc; return acc;
}, [] as Array<Required<SectionRecord>>); }, [] as SectionRecord[]);
}; };
const sections = [...left, ...right]; const sections = [...left, ...right];

View File

@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']); const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
const path: string = get(payload, 'path', ''); const path: string = get(payload, 'path', 'sections.custom');
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]); const isEditMode = useMemo(() => !!item, [item]);
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]); const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]); const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
@ -260,9 +261,9 @@ const CustomModal: React.FC = () => {
multiline multiline
minRows={3} minRows={3}
maxRows={6} maxRows={6}
label={t<string>('builder.common.form.summary.label')}
className="col-span-2" className="col-span-2"
error={!!fieldState.error} error={!!fieldState.error}
label={t<string>('builder.common.form.summary.label')}
helperText={fieldState.error?.message || <MarkdownSupported />} helperText={fieldState.error?.message || <MarkdownSupported />}
{...field} {...field}
/> />

View File

@ -48,12 +48,12 @@ const WorkModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']); const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']);
const path: string = get(payload, 'path', 'sections.work'); const path: string = get(payload, 'path', 'sections.work');
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const isEditMode = useMemo(() => !!item, [item]); const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]); const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);

View File

@ -10,75 +10,74 @@
"dependencies": { "dependencies": {
"@beam-australia/react-env": "^3.1.1", "@beam-australia/react-env": "^3.1.1",
"@date-io/dayjs": "^2.16.0", "@date-io/dayjs": "^2.16.0",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.5",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.10.5",
"@hello-pangea/dnd": "^16.0.1", "@hello-pangea/dnd": "^16.0.1",
"@hookform/resolvers": "2.9.9", "@hookform/resolvers": "2.9.10",
"@monaco-editor/react": "^4.4.6", "@monaco-editor/react": "^4.4.6",
"@mui/icons-material": "^5.10.9", "@mui/icons-material": "^5.10.9",
"@mui/lab": "^5.0.0-alpha.103", "@mui/lab": "^5.0.0-alpha.107",
"@mui/material": "^5.10.9", "@mui/material": "^5.10.13",
"@mui/system": "^5.10.9", "@mui/system": "^5.10.13",
"@mui/x-date-pickers": "5.0.4", "@mui/x-date-pickers": "5.0.8",
"@next/env": "^12.3.1", "@next/env": "^13.0.3",
"@react-oauth/google": "^0.2.8", "@react-oauth/google": "^0.4.0",
"@reduxjs/toolkit": "^1.8.6", "@reduxjs/toolkit": "^1.9.0",
"axios": "^1.1.2", "axios": "^1.1.3",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.6",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"joi": "^17.6.3", "joi": "^17.7.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"md5-hex": "^4.0.0", "md5-hex": "^4.0.0",
"monaco-editor": "^0.34.0", "monaco-editor": "^0.34.1",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"next": "12.3.1", "next": "13.0.3",
"next-i18next": "^12.1.0", "next-i18next": "^12.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dnd": "16.0.1", "react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1", "react-dnd-html5-backend": "16.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.37.0", "react-hook-form": "^7.39.3",
"react-hot-toast": "2.4.0", "react-hot-toast": "2.4.0",
"react-hotkeys-hook": "^3.4.7",
"react-icons": "^4.6.0", "react-icons": "^4.6.0",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-query": "^3.39.2", "react-query": "^3.39.2",
"react-redux": "^8.0.4", "react-redux": "^8.0.5",
"react-zoom-pan-pinch": "^2.1.3", "react-zoom-pan-pinch": "^2.1.3",
"redux": "^4.2.0", "redux": "^4.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.2.1", "redux-saga": "^1.2.1",
"redux-undo": "^1.0.1", "redux-undo": "^1.0.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sharp": "^0.31.1", "sharp": "^0.31.2",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"webfontloader": "^1.6.28" "webfontloader": "^1.6.28"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19.3", "@babel/core": "^7.20.2",
"@reactive-resume/schema": "workspace:*", "@reactive-resume/schema": "workspace:*",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"@tailwindcss/typography": "^0.5.7", "@tailwindcss/typography": "^0.5.8",
"@types/downloadjs": "^1.4.3", "@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.188",
"@types/node": "^18.11.0", "@types/node": "^18.11.9",
"@types/react": "^18.0.21", "@types/react": "^18.0.25",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.8",
"@types/react-redux": "^7.1.24", "@types/react-redux": "^7.1.24",
"@types/tailwindcss": "^3.0.11", "@types/tailwindcss": "^3.0.11",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/webfontloader": "^1.6.35", "@types/webfontloader": "^1.6.35",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.13",
"csstype": "^3.1.1", "csstype": "^3.1.1",
"eslint-config-next": "^12.3.1", "eslint-config-next": "^13.0.3",
"eslint-plugin-tailwindcss": "^3.6.2", "eslint-plugin-tailwindcss": "^3.6.2",
"next-sitemap": "^3.1.25", "next-sitemap": "^3.1.31",
"postcss": "^8.4.18", "postcss": "^8.4.19",
"sass": "^1.55.0", "sass": "^1.56.1",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.2.4",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }

View File

@ -17,13 +17,11 @@ import { fetchResumes } from '@/services/resume';
import { useAppDispatch } from '@/store/hooks'; import { useAppDispatch } from '@/store/hooks';
import styles from '@/styles/pages/Dashboard.module.scss'; import styles from '@/styles/pages/Dashboard.module.scss';
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => { export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])), ...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
}, },
}; });
};
const Dashboard: NextPage = () => { const Dashboard: NextPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -48,9 +46,7 @@ const Dashboard: NextPage = () => {
<header> <header>
<Link href="/"> <Link href="/">
<a>
<Logo size={40} /> <Logo size={40} />
</a>
</Link> </Link>
<Avatar size={40} /> <Avatar size={40} />
@ -58,15 +54,15 @@ const Dashboard: NextPage = () => {
<main className={styles.resumes}> <main className={styles.resumes}>
<ResumeCard <ResumeCard
modal="dashboard.create-resume"
icon={Add} icon={Add}
modal="dashboard.create-resume"
title={t<string>('dashboard.create-resume.title')} title={t<string>('dashboard.create-resume.title')}
subtitle={t<string>('dashboard.create-resume.subtitle')} subtitle={t<string>('dashboard.create-resume.subtitle')}
/> />
<ResumeCard <ResumeCard
modal="dashboard.import-external"
icon={ImportExport} icon={ImportExport}
modal="dashboard.import-external"
title={t<string>('dashboard.import-external.title')} title={t<string>('dashboard.import-external.title')}
subtitle={t<string>('dashboard.import-external.subtitle')} subtitle={t<string>('dashboard.import-external.subtitle')}
/> />

View File

@ -22,13 +22,11 @@ import styles from '@/styles/pages/Home.module.scss';
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants'; import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => { export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])), ...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
}, },
}; });
};
const Home: NextPage = () => { const Home: NextPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -39,11 +37,8 @@ const Home: NextPage = () => {
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn); const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } })); const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } })); const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } }));
const handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' })); const handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' }));
const handleLogout = () => dispatch(logout()); const handleLogout = () => dispatch(logout());
return ( return (
@ -117,7 +112,13 @@ const Home: NextPage = () => {
<div className={styles.screenshots}> <div className={styles.screenshots}>
{screenshots.map(({ src, alt }) => ( {screenshots.map(({ src, alt }) => (
<a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer"> <a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer">
<Image src={src} alt={alt} layout="fill" objectFit="cover" /> <Image
fill
src={src}
alt={alt}
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</a> </a>
))} ))}
</div> </div>
@ -186,7 +187,13 @@ const Home: NextPage = () => {
<section className={styles.section}> <section className={styles.section}>
<a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer"> <a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer">
<Image src={`/images/sponsors/${theme=="dark"?"digitalocean":"digitaloceanLight"}.svg`} alt="Powered By DigitalOcean" width={200} height={40} /> <Image
src={`/images/sponsors/${theme == 'dark' ? 'digitalocean' : 'digitaloceanLight'}.svg`}
style={{ width: 200, height: 40, objectFit: 'contain' }}
alt="Powered By DigitalOcean"
width={200}
height={40}
/>
</a> </a>
</section> </section>

View File

@ -70,7 +70,11 @@ const Preview: NextPage<Props> = ({ shortId }) => {
const handleDownload = async () => { const handleDownload = async () => {
try { try {
const url = await mutateAsync({ username: resume.user.username, slug: resume.slug, lastUpdated: dayjs(resume.updatedAt).unix().toString() }); const url = await mutateAsync({
username: resume.user.username,
slug: resume.slug,
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
});
download(url); download(url);
} catch { } catch {

View File

@ -7,5 +7,8 @@ export type PrintResumeAsPdfParams = {
}; };
export const printResumeAsPdf = (printResumeAsPdfParams: PrintResumeAsPdfParams): Promise<string> => export const printResumeAsPdf = (printResumeAsPdfParams: PrintResumeAsPdfParams): Promise<string> =>
axios.get(`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}?lastUpdated=${printResumeAsPdfParams.lastUpdated}`) axios
.get(
`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}?lastUpdated=${printResumeAsPdfParams.lastUpdated}`
)
.then((res) => res.data); .then((res) => res.data);

View File

@ -40,7 +40,7 @@ export const resumeSlice = createSlice({
addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => { addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => {
const { path, value } = action.payload; const { path, value } = action.payload;
const id = uuidv4(); const id = uuidv4();
const list = get(state, path, []); const list: ListItem[] = get(state, path, []);
const item = merge(value, { id }); const item = merge(value, { id });
list.push(item); list.push(item);

View File

@ -18,7 +18,10 @@ import {
const DEBOUNCE_WAIT = 1000; const DEBOUNCE_WAIT = 1000;
const debouncedSync = debounce((resume: Resume, dispatch: AppDispatch) => updateResume(resume).then((resume) => dispatch(setResume(resume))), DEBOUNCE_WAIT); const debouncedSync = debounce(
(resume: Resume, dispatch: AppDispatch) => updateResume(resume).then((resume) => dispatch(setResume(resume))),
DEBOUNCE_WAIT
);
function* handleSync(dispatch: AppDispatch) { function* handleSync(dispatch: AppDispatch) {
const resume: Resume = yield select((state: RootState) => state.resume.present); const resume: Resume = yield select((state: RootState) => state.resume.present);
@ -27,9 +30,8 @@ function* handleSync(dispatch: AppDispatch) {
} }
function* syncSaga(dispatch: AppDispatch) { function* syncSaga(dispatch: AppDispatch) {
yield takeLatest( yield takeLatest([setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection], () =>
[setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection], handleSync(dispatch)
() => handleSync(dispatch)
); );
} }

View File

@ -26,7 +26,11 @@
} }
a { a {
@apply cursor-pointer font-medium hover:underline; @apply cursor-pointer font-medium;
}
.markdown {
@apply prose prose-sm leading-relaxed prose-ul:p-0 prose-ul:my-0 prose-p:my-0;
} }
} }

View File

@ -52,7 +52,7 @@
@apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6; @apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6;
.image { .image {
@apply relative h-64 rounded hover:opacity-75; @apply relative h-48 rounded hover:opacity-75;
@apply border-2 dark:border-neutral-700; @apply border-2 dark:border-neutral-700;
} }
} }

View File

@ -17,7 +17,7 @@ const Castform: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -6,7 +6,7 @@ import { useMemo } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]); const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]);
return ( return (

View File

@ -19,7 +19,7 @@ export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics (state) => state.resume.present.basics
); );
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]); const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
@ -44,13 +44,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -18,7 +18,7 @@ const Gengar: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]); const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<h3 <h3

View File

@ -20,7 +20,7 @@ export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics (state) => state.resume.present.basics
); );
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -13,7 +13,7 @@ type Props = {
}; };
const BadgeDisplay: React.FC<Props> = ({ items }) => { const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null; if (!isArray(items) || isEmpty(items)) return null;

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<h3 <h3

View File

@ -22,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -46,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -12,7 +12,7 @@ type Props = {
}; };
const BadgeDisplay: React.FC<Props> = ({ items }) => { const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null; if (!isArray(items) || isEmpty(items)) return null;

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<h2 <h2

View File

@ -16,7 +16,7 @@ const Masthead: React.FC = () => {
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics (state) => state.resume.present.basics
); );
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<div> <div>

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} className="mb-2 grid gap-1"> <div key={id} className="mb-2 grid gap-1">

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}> <h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}>

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return ( return (
<h3 <h3

View File

@ -62,7 +62,7 @@ export const MastheadSidebar: React.FC = () => {
}; };
export const MastheadMain: React.FC = () => { export const MastheadMain: React.FC = () => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {})); const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const { name, summary, headline } = useAppSelector((state) => state.resume.present.basics); const { name, summary, headline } = useAppSelector((state) => state.resume.present.basics);

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath), subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath), headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath), keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'), url: string = get(item, 'url', ''),
summary: string = get(item, 'summary'), level: string = get(item, 'level', ''),
level: string = get(item, 'level'), phone: string = get(item, 'phone', ''),
levelNum: number = get(item, 'levelNum'), email: string = get(item, 'email', ''),
phone: string = get(item, 'phone'), summary: string = get(item, 'summary', ''),
email: string = get(item, 'email'), levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date', ''), dateFormat);
return ( return (
<div key={id} id={id} className="grid gap-1"> <div key={id} id={id} className="grid gap-1">

View File

@ -9,9 +9,9 @@ const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
const loadFonts = useCallback(async () => { const loadFonts = useCallback(async () => {
const WebFont = (await import('webfontloader')).default; const WebFont = (await import('webfontloader')).default;
const families = Object.values<string[]>(typography.family).reduce( const families = Object.values(typography.family).reduce(
(acc, family) => [...acc, `${family}:400,600,700`], (acc, family) => [...acc, `${family}:400,600,700`],
[] [] as string[]
); );
WebFont.load({ google: { families } }); WebFont.load({ google: { families } });

View File

@ -1,17 +0,0 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { toggleSidebar } from '@/store/build/buildSlice';
import { useAppDispatch } from '@/store/hooks';
const HotkeysWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const dispatch = useAppDispatch();
useHotkeys('ctrl+/, cmd+/', () => {
dispatch(toggleSidebar({ sidebar: 'left' }));
dispatch(toggleSidebar({ sidebar: 'right' }));
});
return <>{children}</>;
};
export default HotkeysWrapper;

View File

@ -1,17 +1,14 @@
import DateWrapper from './DateWrapper'; import DateWrapper from './DateWrapper';
import FontWrapper from './FontWrapper'; import FontWrapper from './FontWrapper';
import HotkeysWrapper from './HotkeysWrapper';
import ThemeWrapper from './ThemeWrapper'; import ThemeWrapper from './ThemeWrapper';
const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
return ( return (
<ThemeWrapper> <ThemeWrapper>
<FontWrapper> <FontWrapper>
<HotkeysWrapper>
<DateWrapper> <DateWrapper>
<>{children}</> <>{children}</>
</DateWrapper> </DateWrapper>
</HotkeysWrapper>
</FontWrapper> </FontWrapper>
</ThemeWrapper> </ThemeWrapper>
); );

View File

@ -56,6 +56,7 @@ services:
- STORAGE_URL_PREFIX= - STORAGE_URL_PREFIX=
- STORAGE_ACCESS_KEY= - STORAGE_ACCESS_KEY=
- STORAGE_SECRET_KEY= - STORAGE_SECRET_KEY=
- PDF_DELETION_TIME=
client: client:
image: amruthpillai/reactive-resume:client-latest image: amruthpillai/reactive-resume:client-latest

View File

@ -1,6 +1,6 @@
{ {
"name": "reactive-resume", "name": "reactive-resume",
"version": "3.6.8+1", "version": "3.6.9",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "env-cmd --silent turbo run dev", "dev": "env-cmd --silent turbo run dev",
@ -17,12 +17,12 @@
], ],
"dependencies": { "dependencies": {
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"turbo": "^1.5.6" "turbo": "^1.6.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.40.0", "@typescript-eslint/parser": "^5.42.1",
"eslint": "^8.25.0", "eslint": "^8.27.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",

2595
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"build": "tsc" "build": "tsc"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.25.0", "eslint": "^8.27.0",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }

View File

@ -151,5 +151,5 @@ export type Section = {
columns: number; columns: number;
visible: boolean; visible: boolean;
items: ListItem[]; items: ListItem[];
isDuplicated: boolean; isDuplicated?: boolean;
}; };

View File

@ -8,29 +8,29 @@
"start": "node dist/main" "start": "node dist/main"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.188.0", "@aws-sdk/client-s3": "^3.209.0",
"@nestjs/axios": "^0.1.0", "@nestjs/axios": "^1.0.0",
"@nestjs/common": "^9.1.4", "@nestjs/common": "^9.2.0",
"@nestjs/config": "^2.2.0", "@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.1.4", "@nestjs/core": "^9.2.0",
"@nestjs/jwt": "^9.0.0", "@nestjs/jwt": "^9.0.0",
"@nestjs/mapped-types": "^1.2.0", "@nestjs/mapped-types": "^1.2.0",
"@nestjs/passport": "^9.0.0", "@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.1.4", "@nestjs/platform-express": "^9.2.0",
"@nestjs/schedule": "^2.1.0", "@nestjs/schedule": "^2.1.0",
"@nestjs/serve-static": "^3.0.0", "@nestjs/serve-static": "^3.0.0",
"@nestjs/terminus": "^9.1.2", "@nestjs/terminus": "^9.1.2",
"@nestjs/typeorm": "^9.0.1", "@nestjs/typeorm": "^9.0.1",
"@types/passport": "^1.0.11", "@types/passport": "^1.0.11",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cache-manager": "^5.0.1", "cache-manager": "^5.1.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"csvtojson": "^2.0.10", "csvtojson": "^2.0.10",
"dayjs": "^1.11.5", "dayjs": "^1.11.6",
"google-auth-library": "^8.5.2", "google-auth-library": "^8.7.0",
"joi": "^17.6.3", "joi": "^17.7.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
@ -49,15 +49,15 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.1.4", "@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^9.0.3", "@nestjs/schematics": "^9.0.3",
"@reactive-resume/schema": "workspace:*", "@reactive-resume/schema": "workspace:*",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.188",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.11.0", "@types/node": "^18.11.9",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport-jwt": "^3.0.7", "@types/passport-jwt": "^3.0.7",
"@types/passport-local": "^1.0.34", "@types/passport-local": "^1.0.34",
@ -67,6 +67,6 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0", "tsconfig-paths": "^4.1.0",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"webpack": "^5.74.0" "webpack": "^5.75.0"
} }
} }

View File

@ -55,7 +55,7 @@ const validationSchema = Joi.object({
STORAGE_SECRET_KEY: Joi.string().allow(''), STORAGE_SECRET_KEY: Joi.string().allow(''),
// Cache // Cache
PDF_DELETION_TIME: Joi.number().default(6 * 24 * 60 * 60 * 1000), // 6 days PDF_DELETION_TIME: Joi.number().default(4 * 24 * 60 * 60 * 1000), // 4 days
}); });
@Module({ @Module({

View File

@ -3,11 +3,7 @@ import { HealthCheck, HealthCheckService, HttpHealthIndicator, TypeOrmHealthIndi
@Controller('health') @Controller('health')
export class HealthController { export class HealthController {
constructor( constructor(private health: HealthCheckService, private db: TypeOrmHealthIndicator) {}
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private http: HttpHealthIndicator
) {}
@Get() @Get()
@HealthCheck() @HealthCheck()

View File

@ -7,7 +7,11 @@ export class PrinterController {
constructor(private readonly printerService: PrinterService) {} constructor(private readonly printerService: PrinterService) {}
@Get('/:username/:slug') @Get('/:username/:slug')
printAsPdf(@Param('username') username: string, @Param('slug') slug: string, @Query('lastUpdated') lastUpdated: string): Promise<string> { printAsPdf(
@Param('username') username: string,
@Param('slug') slug: string,
@Query('lastUpdated') lastUpdated: string
): Promise<string> {
return this.printerService.printAsPdf(username, slug, lastUpdated); return this.printerService.printAsPdf(username, slug, lastUpdated);
} }
} }

View File

@ -96,7 +96,7 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
await mkdir(directory, { recursive: true }); await mkdir(directory, { recursive: true });
await writeFile(join(directory, filename), pdfBytes); await writeFile(join(directory, filename), pdfBytes);
// Delete PDF artifacts after pdfDeletionTime ms // Delete PDF artifacts after `pdfDeletionTime` ms
const timeout = setTimeout(async () => { const timeout = setTimeout(async () => {
try { try {
await unlink(join(directory, filename)); await unlink(join(directory, filename));