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
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" ]

View File

@ -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]);

View File

@ -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>

View File

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

View File

@ -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 }));

View File

@ -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 />

View File

@ -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>

View File

@ -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}>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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];

View File

@ -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}
/>

View File

@ -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]);

View File

@ -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"
}
}

View File

@ -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')}
/>

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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)
);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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]);

View File

@ -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 (

View File

@ -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]);

View File

@ -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">

View File

@ -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]);

View File

@ -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

View File

@ -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]);

View File

@ -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">

View File

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

View File

@ -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

View File

@ -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">

View File

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

View File

@ -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">

View File

@ -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

View File

@ -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>

View File

@ -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">

View File

@ -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 }}>

View File

@ -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">

View File

@ -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

View File

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

View File

@ -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">

View File

@ -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;
}

View File

@ -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 } });

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

View File

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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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"
}
}

View File

@ -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({

View File

@ -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()

View File

@ -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);
}
}

View File

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