mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-12 15:52:56 +10:00
release: v3.6.9
This commit is contained in:
@ -46,6 +46,6 @@ EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
|
||||
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" ]
|
||||
@ -26,8 +26,8 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const theme: ThemeConfig = get(resume, 'metadata.theme');
|
||||
const customCSS: CustomCSS = get(resume, 'metadata.css');
|
||||
const template: string = get(resume, 'metadata.template');
|
||||
const pageConfig: PageConfig = get(resume, 'metadata.page');
|
||||
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 typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
|
||||
|
||||
@ -111,9 +111,7 @@ const LeftSidebar = () => {
|
||||
<nav className="overflow-y-scroll">
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<a className="inline-flex">
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
</Link>
|
||||
<Divider />
|
||||
</div>
|
||||
@ -132,7 +130,7 @@ const LeftSidebar = () => {
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||
<IconButton onClick={() => handleClick(id)}>
|
||||
<IconButton onClick={() => id && handleClick(id)}>
|
||||
<Star />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@ -79,7 +79,7 @@ const Section: React.FC<Props> = ({
|
||||
visible: true,
|
||||
columns: 2,
|
||||
items: [],
|
||||
isDuplicated: true
|
||||
isDuplicated: true,
|
||||
};
|
||||
|
||||
dispatch(duplicateSection({ value: newSection, type }));
|
||||
|
||||
@ -17,7 +17,9 @@ const CustomCSS = () => {
|
||||
|
||||
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) => {
|
||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||
|
||||
@ -47,8 +47,8 @@ const Settings = () => {
|
||||
const id: number = useMemo(() => get(resume, 'id'), [resume]);
|
||||
const slug: string = useMemo(() => get(resume, 'slug'), [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 pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||
|
||||
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||
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')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List disablePadding>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
@ -212,7 +212,7 @@ const Settings = () => {
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleLoadSampleData}>
|
||||
<ListItemIcon>
|
||||
<Anchor />
|
||||
@ -224,7 +224,7 @@ const Settings = () => {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleResetResume}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever />
|
||||
|
||||
@ -31,7 +31,14 @@ const Templates = () => {
|
||||
<div key={template.id} className={styles.template}>
|
||||
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@ -16,9 +16,7 @@ type Props = {
|
||||
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(setModalState({ modal, state: { open: true } }));
|
||||
};
|
||||
const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
|
||||
|
||||
return (
|
||||
<section className={styles.resume}>
|
||||
|
||||
@ -115,9 +115,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
}}
|
||||
>
|
||||
<ButtonBase className={styles.preview}>
|
||||
{resume.image ? (
|
||||
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
|
||||
) : null}
|
||||
{resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
|
||||
</ButtonBase>
|
||||
</Link>
|
||||
|
||||
|
||||
@ -47,9 +47,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
alt={user?.name}
|
||||
className={styles.avatar}
|
||||
src={getGravatarUrl(email, size)}
|
||||
alt={user?.name ?? 'User Avatar'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ type Props = {
|
||||
size?: 256 | 64 | 48 | 40 | 32;
|
||||
};
|
||||
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => {
|
||||
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />;
|
||||
};
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => (
|
||||
<Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
|
||||
@ -75,54 +75,121 @@ export const left: SidebarSection[] = [
|
||||
{
|
||||
id: 'education',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
icon: <VolunteerActivism />,
|
||||
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',
|
||||
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',
|
||||
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 = (
|
||||
sections: Record<string, SectionRecord>,
|
||||
type: SectionType
|
||||
): Array<Required<SectionRecord>> => {
|
||||
export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
@ -186,10 +250,10 @@ export const getSectionsByType = (
|
||||
}
|
||||
|
||||
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 [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
@ -198,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Array<Required<SectionRecord>>);
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
const sections = [...left, ...right];
|
||||
|
||||
@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
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 path: string = get(payload, 'path', '');
|
||||
const path: string = get(payload, 'path', 'sections.custom');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
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 editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
|
||||
@ -260,9 +261,9 @@ const CustomModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@ -48,12 +48,12 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
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 path: string = get(payload, 'path', 'sections.work');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
|
||||
@ -10,75 +10,74 @@
|
||||
"dependencies": {
|
||||
"@beam-australia/react-env": "^3.1.1",
|
||||
"@date-io/dayjs": "^2.16.0",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@emotion/css": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@hello-pangea/dnd": "^16.0.1",
|
||||
"@hookform/resolvers": "2.9.9",
|
||||
"@hookform/resolvers": "2.9.10",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/lab": "^5.0.0-alpha.103",
|
||||
"@mui/material": "^5.10.9",
|
||||
"@mui/system": "^5.10.9",
|
||||
"@mui/x-date-pickers": "5.0.4",
|
||||
"@next/env": "^12.3.1",
|
||||
"@react-oauth/google": "^0.2.8",
|
||||
"@reduxjs/toolkit": "^1.8.6",
|
||||
"axios": "^1.1.2",
|
||||
"@mui/lab": "^5.0.0-alpha.107",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/system": "^5.10.13",
|
||||
"@mui/x-date-pickers": "5.0.8",
|
||||
"@next/env": "^13.0.3",
|
||||
"@react-oauth/google": "^0.4.0",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
"axios": "^1.1.3",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.5",
|
||||
"dayjs": "^1.11.6",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.3",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.3.1",
|
||||
"next": "13.0.3",
|
||||
"next-i18next": "^12.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.37.0",
|
||||
"react-hook-form": "^7.39.3",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.4",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-undo": "^1.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.1",
|
||||
"sharp": "^0.31.2",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint-config-next": "^12.3.1",
|
||||
"eslint-config-next": "^13.0.3",
|
||||
"eslint-plugin-tailwindcss": "^3.6.2",
|
||||
"next-sitemap": "^3.1.25",
|
||||
"postcss": "^8.4.18",
|
||||
"sass": "^1.55.0",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"next-sitemap": "^3.1.31",
|
||||
"postcss": "^8.4.19",
|
||||
"sass": "^1.56.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,13 +17,11 @@ import { fetchResumes } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -48,9 +46,7 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Avatar size={40} />
|
||||
@ -58,15 +54,15 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<main className={styles.resumes}>
|
||||
<ResumeCard
|
||||
modal="dashboard.create-resume"
|
||||
icon={Add}
|
||||
modal="dashboard.create-resume"
|
||||
title={t<string>('dashboard.create-resume.title')}
|
||||
subtitle={t<string>('dashboard.create-resume.subtitle')}
|
||||
/>
|
||||
|
||||
<ResumeCard
|
||||
modal="dashboard.import-external"
|
||||
icon={ImportExport}
|
||||
modal="dashboard.import-external"
|
||||
title={t<string>('dashboard.import-external.title')}
|
||||
subtitle={t<string>('dashboard.import-external.subtitle')}
|
||||
/>
|
||||
|
||||
@ -22,13 +22,11 @@ import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -39,11 +37,8 @@ const Home: NextPage = () => {
|
||||
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 handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' }));
|
||||
|
||||
const handleLogout = () => dispatch(logout());
|
||||
|
||||
return (
|
||||
@ -117,7 +112,13 @@ const Home: NextPage = () => {
|
||||
<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" />
|
||||
<Image
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@ -186,7 +187,13 @@ const Home: NextPage = () => {
|
||||
|
||||
<section className={styles.section}>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
|
||||
@ -70,7 +70,11 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
|
||||
const handleDownload = async () => {
|
||||
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);
|
||||
} catch {
|
||||
|
||||
@ -7,5 +7,8 @@ export type PrintResumeAsPdfParams = {
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@ -40,7 +40,7 @@ export const resumeSlice = createSlice({
|
||||
addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => {
|
||||
const { path, value } = action.payload;
|
||||
const id = uuidv4();
|
||||
const list = get(state, path, []);
|
||||
const list: ListItem[] = get(state, path, []);
|
||||
const item = merge(value, { id });
|
||||
|
||||
list.push(item);
|
||||
|
||||
@ -18,7 +18,10 @@ import {
|
||||
|
||||
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) {
|
||||
const resume: Resume = yield select((state: RootState) => state.resume.present);
|
||||
@ -27,9 +30,8 @@ function* handleSync(dispatch: AppDispatch) {
|
||||
}
|
||||
|
||||
function* syncSaga(dispatch: AppDispatch) {
|
||||
yield takeLatest(
|
||||
[setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection],
|
||||
() => handleSync(dispatch)
|
||||
yield takeLatest([setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection], () =>
|
||||
handleSync(dispatch)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,11 @@
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
@apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6;
|
||||
|
||||
.image {
|
||||
@apply relative h-64 rounded hover:opacity-75;
|
||||
@apply relative h-48 rounded hover:opacity-75;
|
||||
@apply border-2 dark:border-neutral-700;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const Castform: React.FC<PageProps> = ({ page }) => {
|
||||
const isFirstPage = useMemo(() => page === 0, [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 color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -6,7 +6,7 @@ import { useMemo } from 'react';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
|
||||
@ -19,7 +19,7 @@ export const MastheadSidebar: React.FC = () => {
|
||||
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(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 color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
|
||||
@ -44,13 +44,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -18,7 +18,7 @@ const Gengar: React.FC<PageProps> = ({ page }) => {
|
||||
const isFirstPage = useMemo(() => page === 0, [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 backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]);
|
||||
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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 (
|
||||
<h3
|
||||
|
||||
@ -20,7 +20,7 @@ export const MastheadSidebar: React.FC = () => {
|
||||
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(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 iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
if (!isArray(items) || isEmpty(items)) return null;
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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 (
|
||||
<h3
|
||||
|
||||
@ -22,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -46,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -12,7 +12,7 @@ type Props = {
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
if (!isArray(items) || isEmpty(items)) return null;
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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 (
|
||||
<h2
|
||||
|
||||
@ -16,7 +16,7 @@ const Masthead: React.FC = () => {
|
||||
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(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 (
|
||||
<div>
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} className="mb-2 grid gap-1">
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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 (
|
||||
<h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}>
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
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 (
|
||||
<h3
|
||||
|
||||
@ -62,7 +62,7 @@ export const MastheadSidebar: 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 { name, summary, headline } = useAppSelector((state) => state.resume.present.basics);
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -53,7 +53,7 @@ export const getSectionById = (id: string, Section: React.FC<SectionProps>): JSX
|
||||
// Check if section id is a predefined seciton in config
|
||||
const predefinedSection = get(sectionMap(Section), id);
|
||||
|
||||
if(predefinedSection) {
|
||||
if (predefinedSection) {
|
||||
return predefinedSection;
|
||||
}
|
||||
|
||||
|
||||
@ -9,9 +9,9 @@ const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
|
||||
|
||||
const loadFonts = useCallback(async () => {
|
||||
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`],
|
||||
[]
|
||||
[] as string[]
|
||||
);
|
||||
|
||||
WebFont.load({ google: { families } });
|
||||
|
||||
@ -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;
|
||||
@ -1,17 +1,14 @@
|
||||
import DateWrapper from './DateWrapper';
|
||||
import FontWrapper from './FontWrapper';
|
||||
import HotkeysWrapper from './HotkeysWrapper';
|
||||
import ThemeWrapper from './ThemeWrapper';
|
||||
|
||||
const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<FontWrapper>
|
||||
<HotkeysWrapper>
|
||||
<DateWrapper>
|
||||
<>{children}</>
|
||||
</DateWrapper>
|
||||
</HotkeysWrapper>
|
||||
</FontWrapper>
|
||||
</ThemeWrapper>
|
||||
);
|
||||
|
||||
@ -56,6 +56,7 @@ services:
|
||||
- STORAGE_URL_PREFIX=
|
||||
- STORAGE_ACCESS_KEY=
|
||||
- STORAGE_SECRET_KEY=
|
||||
- PDF_DELETION_TIME=
|
||||
|
||||
client:
|
||||
image: amruthpillai/reactive-resume:client-latest
|
||||
|
||||
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reactive-resume",
|
||||
"version": "3.6.8+1",
|
||||
"version": "3.6.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent turbo run dev",
|
||||
@ -17,12 +17,12 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"env-cmd": "^10.1.0",
|
||||
"turbo": "^1.5.6"
|
||||
"turbo": "^1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint": "^8.25.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
|
||||
2595
pnpm-lock.yaml
generated
2595
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.25.0",
|
||||
"eslint": "^8.27.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,5 +151,5 @@ export type Section = {
|
||||
columns: number;
|
||||
visible: boolean;
|
||||
items: ListItem[];
|
||||
isDuplicated: boolean;
|
||||
isDuplicated?: boolean;
|
||||
};
|
||||
|
||||
@ -8,29 +8,29 @@
|
||||
"start": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.188.0",
|
||||
"@nestjs/axios": "^0.1.0",
|
||||
"@nestjs/common": "^9.1.4",
|
||||
"@aws-sdk/client-s3": "^3.209.0",
|
||||
"@nestjs/axios": "^1.0.0",
|
||||
"@nestjs/common": "^9.2.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.1.4",
|
||||
"@nestjs/core": "^9.2.0",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/mapped-types": "^1.2.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.1.4",
|
||||
"@nestjs/platform-express": "^9.2.0",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
"@nestjs/serve-static": "^3.0.0",
|
||||
"@nestjs/terminus": "^9.1.2",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@types/passport": "^1.0.11",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.0.1",
|
||||
"cache-manager": "^5.1.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dayjs": "^1.11.5",
|
||||
"google-auth-library": "^8.5.2",
|
||||
"joi": "^17.6.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"google-auth-library": "^8.7.0",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^3.3.4",
|
||||
@ -49,15 +49,15 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.4",
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
@ -67,6 +67,6 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.4",
|
||||
"webpack": "^5.74.0"
|
||||
"webpack": "^5.75.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ const validationSchema = Joi.object({
|
||||
STORAGE_SECRET_KEY: Joi.string().allow(''),
|
||||
|
||||
// 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({
|
||||
|
||||
@ -3,11 +3,7 @@ import { HealthCheck, HealthCheckService, HttpHealthIndicator, TypeOrmHealthIndi
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private health: HealthCheckService,
|
||||
private db: TypeOrmHealthIndicator,
|
||||
private http: HttpHealthIndicator
|
||||
) {}
|
||||
constructor(private health: HealthCheckService, private db: TypeOrmHealthIndicator) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
|
||||
@ -7,7 +7,11 @@ export class PrinterController {
|
||||
constructor(private readonly printerService: PrinterService) {}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, filename), pdfBytes);
|
||||
|
||||
// Delete PDF artifacts after pdfDeletionTime ms
|
||||
// Delete PDF artifacts after `pdfDeletionTime` ms
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
await unlink(join(directory, filename));
|
||||
|
||||
Reference in New Issue
Block a user