feat: update app version to 3.6.2

This commit is contained in:
Amruth Pillai
2022-08-26 00:00:18 +02:00
parent 57dd110187
commit 7902f67f4f
90 changed files with 1747 additions and 2060 deletions
+10 -7
View File
@@ -1,15 +1,14 @@
{ {
"root": true,
"ignorePatterns": ["/app"], "ignorePatterns": ["/app"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], "extends": ["plugin:@typescript-eslint/recommended"],
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"], "plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
"rules": { "rules": {
// Unused Imports // ESLint
"no-unused-vars": "off", "no-unused-vars": "off",
// Simple Import Sort
"simple-import-sort/imports": "error", // Unused Imports
"simple-import-sort/exports": "error", "unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [ "unused-imports/no-unused-vars": [
"warn", "warn",
{ {
@@ -19,7 +18,11 @@
"argsIgnorePattern": "^_" "argsIgnorePattern": "^_"
} }
], ],
"unused-imports/no-unused-imports": "error",
// Simple Import Sort
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
// TypeScript ESLint // TypeScript ESLint
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
+11
View File
@@ -2,6 +2,17 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [3.6.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.3...v3.6.2) (2022-08-25)
### Features
- Implement Undo/Redo functionality across the resume builder section
### Improvements
- Update dependencies to the latest version
- Cleanup ESLint configuration, add tailwindCSS formatting
### [3.5.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.2...v3.5.3) (2022-08-11) ### [3.5.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.2...v3.5.3) (2022-08-11)
### [3.5.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.1...v3.5.2) (2022-08-04) ### [3.5.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.1...v3.5.2) (2022-08-04)
+1 -1
View File
@@ -18,7 +18,7 @@ You have complete control over what goes into your resume, how it looks, what co
## Table of Contents ## Table of Contents
- [Reactive Resume](#reactive-resume) - [Reactive Resume](#reactive-resume)
- [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me) - [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Features](#features) - [Features](#features)
- [Languages](#languages) - [Languages](#languages)
+13 -2
View File
@@ -1,9 +1,20 @@
{ {
"extends": ["../.eslintrc.json", "next/core-web-vitals"], "extends": ["../.eslintrc.json", "next/core-web-vitals", "plugin:tailwindcss/recommended"],
"ignorePatterns": [".next", "__ENV.js"], "ignorePatterns": [".next", "__ENV.js"],
"settings": {
"next": {
"rootDir": "client"
}
},
"rules": { "rules": {
// Next.js
"@next/next/no-img-element": "off", "@next/next/no-img-element": "off",
"@next/next/no-sync-scripts": "off", "@next/next/no-sync-scripts": "off",
"@next/next/no-html-link-for-pages": ["error", "pages"]
// React Hooks
"react-hooks/exhaustive-deps": "off",
// Tailwind CSS
"tailwindcss/no-custom-classname": ["warn", { "whitelist": ["preview-mode", "printer-mode", "markdown"] }]
} }
} }
@@ -5,6 +5,8 @@ import {
FilterCenterFocus, FilterCenterFocus,
InsertPageBreak, InsertPageBreak,
Link, Link,
RedoOutlined,
UndoOutlined,
ViewSidebar, ViewSidebar,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
@@ -16,6 +18,7 @@ import { useTranslation } from 'next-i18next';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
import { ActionCreators } from 'redux-undo';
import { ServerError } from '@/services/axios'; import { ServerError } from '@/services/axios';
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer'; import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
@@ -31,14 +34,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
const theme = useTheme(); const theme = useTheme();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const resume = useAppSelector((state) => state.resume);
const isDesktop = useMediaQuery(theme.breakpoints.up('sm')); const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
const pages = useAppSelector((state) => state.resume.metadata.layout);
const { past, present: resume, future } = useAppSelector((state) => state.resume);
const pages = get(resume, 'metadata.layout');
const { left, right } = useAppSelector((state) => state.build.sidebar); const { left, right } = useAppSelector((state) => state.build.sidebar);
const orientation = useAppSelector((state) => state.build.page.orientation); const orientation = useAppSelector((state) => state.build.page.orientation);
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf); const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
const handleUndo = () => dispatch(ActionCreators.undo());
const handleRedo = () => dispatch(ActionCreators.redo());
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine()); const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
const handleTogglePageOrientation = () => dispatch(togglePageOrientation()); const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
@@ -75,6 +82,20 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
})} })}
> >
<div className={styles.controller}> <div className={styles.controller}>
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.undo')}>
<ButtonBase onClick={handleUndo} className={clsx({ 'pointer-events-none opacity-50': past.length < 2 })}>
<UndoOutlined fontSize="medium" />
</ButtonBase>
</Tooltip>
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.redo')}>
<ButtonBase onClick={handleRedo} className={clsx({ 'pointer-events-none opacity-50': future.length === 0 })}>
<RedoOutlined fontSize="medium" />
</ButtonBase>
</Tooltip>
<Divider />
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}> <Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
<ButtonBase onClick={() => zoomIn(0.25)}> <ButtonBase onClick={() => zoomIn(0.25)}>
<ZoomIn fontSize="medium" /> <ZoomIn fontSize="medium" />
@@ -97,17 +118,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
{isDesktop && ( {isDesktop && (
<> <>
{pages.length > 1 && ( <Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}> <ButtonBase
<ButtonBase onClick={handleTogglePageOrientation}> onClick={handleTogglePageOrientation}
{orientation === 'vertical' ? ( className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
<AlignHorizontalCenter fontSize="medium" /> >
) : ( {orientation === 'vertical' ? (
<AlignVerticalCenter fontSize="medium" /> <AlignHorizontalCenter fontSize="medium" />
)} ) : (
</ButtonBase> <AlignVerticalCenter fontSize="medium" />
</Tooltip> )}
)} </ButtonBase>
</Tooltip>
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}> <Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
<ButtonBase onClick={handleTogglePageBreakLine}> <ButtonBase onClick={handleTogglePageBreakLine}>
+1 -1
View File
@@ -13,7 +13,7 @@ import Page from './Page';
const Center = () => { const Center = () => {
const orientation = useAppSelector((state) => state.build.page.orientation); const orientation = useAppSelector((state) => state.build.page.orientation);
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const layout: string[][][] = get(resume, 'metadata.layout'); const layout: string[][][] = get(resume, 'metadata.layout');
if (isEmpty(resume)) return null; if (isEmpty(resume)) return null;
+1 -1
View File
@@ -57,7 +57,7 @@ const Header = () => {
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume); const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const { left, right } = useAppSelector((state) => state.build.sidebar); const { left, right } = useAppSelector((state) => state.build.sidebar);
const name = useMemo(() => get(resume, 'name'), [resume]); const name = useMemo(() => get(resume, 'name'), [resume]);
+1 -1
View File
@@ -20,7 +20,7 @@ type Props = PageProps & {
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => { const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine); const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine);
const theme: Theme = get(resume, 'metadata.theme'); const theme: Theme = get(resume, 'metadata.theme');
@@ -25,7 +25,7 @@ const LeftSidebar = () => {
const isDesktop = useMediaQuery(theme.breakpoints.up('lg')); const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
const sections = useAppSelector((state) => state.resume.sections); const sections = useAppSelector((state) => state.resume.present.sections);
const { open } = useAppSelector((state) => state.build.sidebar.left); const { open } = useAppSelector((state) => state.build.sidebar.left);
const customSections = useMemo(() => getCustomSections(sections), [sections]); const customSections = useMemo(() => getCustomSections(sections), [sections]);
@@ -32,7 +32,7 @@ const Basics = () => {
<PhotoUpload /> <PhotoUpload />
</div> </div>
<div className="grid gap-2 w-full sm:col-span-2"> <div className="grid w-full gap-2 sm:col-span-2">
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" /> <ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}> <Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
@@ -12,7 +12,7 @@ const PhotoFilters = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo')); const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
const size: number = get(photo, 'filters.size', 128); const size: number = get(photo, 'filters.size', 128);
const shape: PhotoShape = get(photo, 'filters.shape', 'square'); const shape: PhotoShape = get(photo, 'filters.shape', 'square');
const grayscale: boolean = get(photo, 'filters.grayscale', false); const grayscale: boolean = get(photo, 'filters.grayscale', false);
@@ -21,8 +21,8 @@ const PhotoUpload: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const id: number = useAppSelector((state) => get(state.resume, 'id')); const id: number = useAppSelector((state) => get(state.resume.present, 'id'));
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo')); const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto); const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
@@ -37,8 +37,8 @@ const Section: React.FC<Props> = ({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector<string>((state) => get(state.resume, `${path}.name`, name)); const heading = useAppSelector<string>((state) => get(state.resume.present, `${path}.name`, name));
const visibility = useAppSelector<boolean>((state) => get(state.resume, `${path}.visible`, true)); const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
const handleAdd = () => { const handleAdd = () => {
const id = path.split('.')[1]; const id = path.split('.')[1];
@@ -18,7 +18,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const columns = useAppSelector<number>((state) => get(state.resume, `${path}.columns`, 2)); const columns = useAppSelector<number>((state) => get(state.resume.present, `${path}.columns`, 2));
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -17,7 +17,7 @@ const CustomCSS = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume, 'metadata.css', {})); const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
const handleChange = (value: string | undefined) => { const handleChange = (value: string | undefined) => {
dispatch(setResumeState({ path: 'metadata.css.value', value })); dispatch(setResumeState({ path: 'metadata.css.value', value }));
@@ -13,7 +13,7 @@ import { useAppSelector } from '@/store/hooks';
const Export = () => { const Export = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf); const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
@@ -1,10 +1,10 @@
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from '@hello-pangea/dnd';
import { Add, Close, Restore } from '@mui/icons-material'; import { Add, Close, Restore } from '@mui/icons-material';
import { Button, IconButton, Tooltip } from '@mui/material'; import { Button, IconButton, Tooltip } from '@mui/material';
import clsx from 'clsx'; import clsx from 'clsx';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get'; import get from 'lodash/get';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from 'react-beautiful-dnd';
import Heading from '@/components/shared/Heading'; import Heading from '@/components/shared/Heading';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
@@ -23,8 +23,8 @@ const Layout = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const layout = useAppSelector((state) => state.resume.metadata.layout); const layout = useAppSelector((state) => state.resume.present.metadata.layout);
const resumeSections = useAppSelector((state) => state.resume.sections); const resumeSections = useAppSelector((state) => state.resume.present.sections);
const onDragEnd = (dropResult: DropResult) => { const onDragEnd = (dropResult: DropResult) => {
const { source: srcLoc, destination: destLoc } = dropResult; const { source: srcLoc, destination: destLoc } = dropResult;
@@ -38,9 +38,9 @@ const Settings = () => {
const [confirmReset, setConfirmReset] = useState(false); const [confirmReset, setConfirmReset] = useState(false);
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const theme = useAppSelector((state) => state.build.theme); const theme = useAppSelector((state) => state.build.theme);
const pages = useAppSelector((state) => state.resume.metadata.layout); const pages = useAppSelector((state) => state.resume.present.metadata.layout);
const breakLine = useAppSelector((state) => state.build.page.breakLine); const breakLine = useAppSelector((state) => state.build.page.breakLine);
const orientation = useAppSelector((state) => state.build.page.orientation); const orientation = useAppSelector((state) => state.build.page.orientation);
@@ -17,7 +17,7 @@ const Sharing = () => {
const [showShortUrl, setShowShortUrl] = useState(false); const [showShortUrl, setShowShortUrl] = useState(false);
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
const isPublic = useMemo(() => get(resume, 'public'), [resume]); const isPublic = useMemo(() => get(resume, 'public'), [resume]);
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]); const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]); const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
@@ -16,7 +16,7 @@ const Templates = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const currentTemplate: string = useAppSelector((state) => get(state.resume, 'metadata.template')); const currentTemplate: string = useAppSelector((state) => get(state.resume.present, 'metadata.template'));
const handleChange = (template: TemplateMeta) => { const handleChange = (template: TemplateMeta) => {
dispatch(setResumeState({ path: 'metadata.template', value: template.id })); dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
@@ -31,7 +31,7 @@ const Templates = () => {
<div key={template.id} className={styles.template}> <div key={template.id} className={styles.template}>
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}> <div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
<ButtonBase onClick={() => handleChange(template)}> <ButtonBase onClick={() => handleChange(template)}>
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" /> <Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
</ButtonBase> </ButtonBase>
</div> </div>
@@ -1,8 +1,8 @@
.container { .container {
@apply grid sm:grid-cols-2 gap-4; @apply grid gap-4 sm:grid-cols-2;
} }
.colorOptions { .colorOptions {
@apply col-span-2 mb-4; @apply col-span-2 mb-4;
@apply grid grid-cols-8 gap-y-2 justify-items-center; @apply grid grid-cols-8 justify-items-center gap-y-2;
} }
@@ -16,7 +16,9 @@ const Theme = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme')); const { background, text, primary } = useAppSelector<ThemeType>((state) =>
get(state.resume.present, 'metadata.theme')
);
const handleChange = (property: string, color: string) => { const handleChange = (property: string, color: string) => {
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color })); dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
@@ -33,7 +33,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume, 'metadata.typography')); const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume.present, 'metadata.typography'));
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, { const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)), select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
@@ -1,9 +1,9 @@
.testimony { .testimony {
@apply grid gap-2; @apply grid gap-2;
@apply border-2 rounded p-4 dark:border-neutral-800; @apply rounded border-2 p-4 dark:border-neutral-800;
blockquote { blockquote {
@apply text-xs leading-normal text-justify opacity-90; @apply text-justify text-xs leading-normal opacity-90;
} }
figcaption { figcaption {
@@ -10,7 +10,7 @@
} }
.header { .header {
@apply sticky top-0 left-0 right-0 z-50 pt-6 bg-neutral-50 dark:bg-neutral-900; @apply sticky top-0 left-0 right-0 z-50 bg-neutral-50 pt-6 dark:bg-neutral-900;
@apply flex items-center justify-between; @apply flex items-center justify-between;
@apply w-full border-b pb-5 dark:border-white/10; @apply w-full border-b pb-5 dark:border-white/10;
@@ -33,7 +33,7 @@
} }
.footer { .footer {
@apply sticky bottom-0 left-0 right-0 z-50 pb-6 bg-neutral-50 dark:bg-neutral-900; @apply sticky bottom-0 left-0 right-0 z-50 bg-neutral-50 pb-6 dark:bg-neutral-900;
@apply flex items-center justify-end gap-x-4; @apply flex items-center justify-end gap-x-4;
@apply w-full border-t pt-5 dark:border-white/10; @apply w-full border-t pt-5 dark:border-white/10;
} }
+2 -2
View File
@@ -32,8 +32,8 @@ const Heading: React.FC<Props> = ({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`, name));
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true)); const visibility = useAppSelector((state) => get(state.resume.present, `${path}.visible`, true));
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
@@ -9,5 +9,5 @@
} }
.language { .language {
@apply py-2 px-4 cursor-pointer text-center hover:underline; @apply cursor-pointer py-2 px-4 text-center hover:underline;
} }
+1 -1
View File
@@ -36,7 +36,7 @@ const List: React.FC<Props> = ({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume, path, [])); const list: Array<ListItemType> = useAppSelector((state) => get(state.resume.present, path, []));
const handleEdit = (item: ListItemType) => { const handleEdit = (item: ListItemType) => {
isFunction(onEdit) && onEdit(item); isFunction(onEdit) && onEdit(item);
+1
View File
@@ -5,6 +5,7 @@ import styles from './Loading.module.scss';
const Loading: React.FC = () => { const Loading: React.FC = () => {
const { isReady } = useRouter(); const { isReady } = useRouter();
const isFetching = useIsFetching(); const isFetching = useIsFetching();
const isMutating = useIsMutating(); const isMutating = useIsMutating();
+1 -1
View File
@@ -21,7 +21,7 @@ interface Props {
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => { const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const stateValue = useAppSelector((state) => get(state.resume, path, '')); const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
useEffect(() => { useEffect(() => {
setValue(stateValue); setValue(stateValue);
@@ -44,7 +44,7 @@ const AwardModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -44,7 +44,7 @@ const CertificateModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -60,7 +60,7 @@ const CustomModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']); const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
const path: string = get(payload, 'path', ''); const path: string = get(payload, 'path', '');
@@ -57,7 +57,7 @@ const EducationModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -35,7 +35,7 @@ const InterestModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -36,7 +36,7 @@ const LanguageModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -53,7 +53,7 @@ const ProjectModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -44,7 +44,7 @@ const PublicationModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -41,7 +41,7 @@ const ReferenceModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -39,7 +39,7 @@ const SkillModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -50,7 +50,7 @@ const VolunteerModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
+1 -1
View File
@@ -50,7 +50,7 @@ const WorkModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`)); const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]); const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
@@ -69,7 +69,8 @@ const CreateResumeModal: React.FC = () => {
try { try {
await mutateAsync({ name, slug, public: isPublic }); await mutateAsync({ name, slug, public: isPublic });
queryClient.invalidateQueries(RESUMES_QUERY); await queryClient.invalidateQueries(RESUMES_QUERY);
handleClose(); handleClose();
} catch (error: any) { } catch (error: any) {
toast.error(error.message); toast.error(error.message);
+15 -18
View File
@@ -13,13 +13,14 @@
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.0", "@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0", "@emotion/styled": "^11.10.0",
"@hello-pangea/dnd": "^16.0.0",
"@hookform/resolvers": "2.9.7", "@hookform/resolvers": "2.9.7",
"@monaco-editor/react": "^4.4.5", "@monaco-editor/react": "^4.4.5",
"@mui/icons-material": "^5.8.4", "@mui/icons-material": "^5.10.2",
"@mui/lab": "^5.0.0-alpha.95", "@mui/lab": "^5.0.0-alpha.96",
"@mui/material": "^5.10.1", "@mui/material": "^5.10.2",
"@mui/system": "^5.10.1", "@mui/system": "^5.10.2",
"@mui/x-date-pickers": "5.0.0-beta.6", "@mui/x-date-pickers": "5.0.0-beta.7",
"@next/env": "^12.2.5", "@next/env": "^12.2.5",
"@react-oauth/google": "^0.2.6", "@react-oauth/google": "^0.2.6",
"@reduxjs/toolkit": "^1.8.5", "@reduxjs/toolkit": "^1.8.5",
@@ -34,12 +35,11 @@
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"next": "12.2.5", "next": "12.2.5",
"next-i18next": "^12.0.0", "next-i18next": "^12.0.0",
"react": "18", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dnd": "16.0.1", "react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1", "react-dnd-html5-backend": "16.0.1",
"react-dom": "18", "react-dom": "^18.2.0",
"react-hook-form": "^7.34.2", "react-hook-form": "^7.34.2",
"react-hot-toast": "2.3.0", "react-hot-toast": "2.3.0",
"react-hotkeys-hook": "^3.4.7", "react-hotkeys-hook": "^3.4.7",
@@ -51,32 +51,29 @@
"redux": "^4.2.0", "redux": "^4.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.2.1", "redux-saga": "^1.2.1",
"redux-undo": "^1.0.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sharp": "^0.30.7", "sharp": "^0.30.7",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "^1.6.28" "webfontloader": "^1.6.28"
}, },
"resolutions": {
"@types/react": "18",
"@types/react-dom": "18"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.10", "@babel/core": "^7.18.13",
"@reactive-resume/schema": "workspace:*", "@reactive-resume/schema": "workspace:*",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/downloadjs": "^1.4.3", "@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.184",
"@types/node": "18.7.9", "@types/node": "^18.7.13",
"@types/react": "18", "@types/react": "^18.0.17",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^18.0.6",
"@types/react-dom": "18",
"@types/react-redux": "^7.1.24", "@types/react-redux": "^7.1.24",
"@types/tailwindcss": "^3.0.11", "@types/tailwindcss": "^3.0.11",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/webfontloader": "^1.6.34", "@types/webfontloader": "^1.6.34",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"csstype": "^3.1.0", "csstype": "^3.1.0",
"eslint-config-next": "12.2.5", "eslint-config-next": "^12.2.5",
"eslint-plugin-tailwindcss": "^3.6.0",
"next-sitemap": "^3.1.21", "next-sitemap": "^3.1.21",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"sass": "^1.54.5", "sass": "^1.54.5",
+1 -1
View File
@@ -51,7 +51,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
useEffect(() => { useEffect(() => {
if (initialData && !isEmpty(initialData)) { if (initialData && !isEmpty(initialData)) {
+1 -1
View File
@@ -56,7 +56,7 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => { const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const resume = useAppSelector((state) => state.resume); const resume = useAppSelector((state) => state.resume.present);
useEffect(() => { useEffect(() => {
if (initialData) dispatch(setResume(initialData)); if (initialData) dispatch(setResume(initialData));
+9
View File
@@ -4,7 +4,9 @@ import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useEffect } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { ActionCreators } from 'redux-undo';
import ResumeCard from '@/components/dashboard/ResumeCard'; import ResumeCard from '@/components/dashboard/ResumeCard';
import ResumePreview from '@/components/dashboard/ResumePreview'; import ResumePreview from '@/components/dashboard/ResumePreview';
@@ -12,6 +14,7 @@ import Avatar from '@/components/shared/Avatar';
import Logo from '@/components/shared/Logo'; import Logo from '@/components/shared/Logo';
import { RESUMES_QUERY } from '@/constants/index'; import { RESUMES_QUERY } from '@/constants/index';
import { fetchResumes } from '@/services/resume'; import { fetchResumes } from '@/services/resume';
import { useAppDispatch } from '@/store/hooks';
import styles from '@/styles/pages/Dashboard.module.scss'; import styles from '@/styles/pages/Dashboard.module.scss';
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => { export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
@@ -25,8 +28,14 @@ export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
const Dashboard: NextPage = () => { const Dashboard: NextPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const { data } = useQuery(RESUMES_QUERY, fetchResumes); const { data } = useQuery(RESUMES_QUERY, fetchResumes);
useEffect(() => {
dispatch(ActionCreators.clearHistory());
}, []);
if (!data) return null; if (!data) return null;
return ( return (
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextPage } from 'next'; import { NextPage } from 'next';
const PrivacyPolicy: NextPage = () => ( const PrivacyPolicy: NextPage = () => (
<div className="mx-auto my-12 prose dark:prose-invert"> <div className="prose mx-auto my-12 dark:prose-invert">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p> <p>
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextPage } from 'next'; import { NextPage } from 'next';
const TermsOfService: NextPage = () => ( const TermsOfService: NextPage = () => (
<div className="mx-auto my-12 prose dark:prose-invert"> <div className="prose mx-auto my-12 dark:prose-invert">
<h1>Terms of Service</h1> <h1>Terms of Service</h1>
<p> <p>
+3 -1
View File
@@ -84,7 +84,9 @@
"toggle-page-break-line": "Toggle Page Break Line", "toggle-page-break-line": "Toggle Page Break Line",
"toggle-sidebars": "Toggle Sidebars", "toggle-sidebars": "Toggle Sidebars",
"zoom-in": "Zoom In", "zoom-in": "Zoom In",
"zoom-out": "Zoom Out" "zoom-out": "Zoom Out",
"undo": "Undo",
"redo": "Redo"
} }
}, },
"header": { "header": {
+2 -1
View File
@@ -1,6 +1,7 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist'; import { persistReducer, persistStore } from 'redux-persist';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import undoable from 'redux-undo';
import authReducer from '@/store/auth/authSlice'; import authReducer from '@/store/auth/authSlice';
import buildReducer from '@/store/build/buildSlice'; import buildReducer from '@/store/build/buildSlice';
@@ -16,7 +17,7 @@ const reducers = combineReducers({
auth: authReducer, auth: authReducer,
modal: modalReducer, modal: modalReducer,
build: buildReducer, build: buildReducer,
resume: resumeReducer, resume: undoable(resumeReducer),
}); });
const persistedReducers = persistReducer({ key: 'root', storage, whitelist: ['auth', 'build'] }, reducers); const persistedReducers = persistReducer({ key: 'root', storage, whitelist: ['auth', 'build'] }, reducers);
+2 -1
View File
@@ -3,6 +3,7 @@ import debounce from 'lodash/debounce';
import { select, takeLatest } from 'redux-saga/effects'; import { select, takeLatest } from 'redux-saga/effects';
import { updateResume } from '@/services/resume'; import { updateResume } from '@/services/resume';
import { RootState } from '@/store/index';
import { import {
addItem, addItem,
@@ -19,7 +20,7 @@ const DEBOUNCE_WAIT = 1000;
const debouncedSync = debounce((resume: Resume) => updateResume(resume), DEBOUNCE_WAIT); const debouncedSync = debounce((resume: Resume) => updateResume(resume), DEBOUNCE_WAIT);
function* handleSync() { function* handleSync() {
const resume: Resume = yield select((state) => state.resume); const resume: Resume = yield select((state: RootState) => state.resume.present);
debouncedSync(resume); debouncedSync(resume);
} }
+3 -3
View File
@@ -2,14 +2,14 @@
@apply m-6 grid gap-8 text-center md:m-8 md:text-left; @apply m-6 grid gap-8 text-center md:m-8 md:text-left;
footer { footer {
@apply flex flex-col gap-8 items-center sm:items-end justify-between sm:flex-row sm:gap-0; @apply flex flex-col items-center justify-between gap-8 sm:flex-row sm:items-end sm:gap-0;
.actions { .actions {
@apply flex gap-2; @apply flex gap-2;
} }
.version > div { .version > div {
@apply text-xs font-medium opacity-50 mt-3; @apply mt-3 text-xs font-medium opacity-50;
} }
} }
} }
@@ -49,7 +49,7 @@
} }
.screenshots { .screenshots {
@apply grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4; @apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6;
.image { .image {
@apply relative h-64 rounded hover:opacity-75; @apply relative h-64 rounded hover:opacity-75;
+2 -2
View File
@@ -16,8 +16,8 @@ import Section from './widgets/Section';
const Castform: React.FC<PageProps> = ({ page }) => { const Castform: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
@@ -6,7 +6,7 @@ import { useMemo } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]); const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]);
return ( return (
@@ -15,11 +15,11 @@ import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
@@ -72,7 +72,7 @@ export const MastheadSidebar: React.FC = () => {
}; };
export const MastheadMain: React.FC = () => { export const MastheadMain: React.FC = () => {
const { summary } = useAppSelector((state) => state.resume.basics); const { summary } = useAppSelector((state) => state.resume.present.basics);
return ( return (
<div className="px-4 pt-4"> <div className="px-4 pt-4">
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,15 +21,17 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Castform_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
@@ -67,7 +70,7 @@ const Section: React.FC<SectionProps> = ({
<div className="grid gap-1"> <div className="grid gap-1">
{level && <span className="opacity-75">{level}</span>} {level && <span className="opacity-75">{level}</span>}
{levelNum > 0 && ( {levelNum > 0 && (
<div className="level flex"> <div className="flex">
{Array.from(Array(5).keys()).map((_, index) => ( {Array.from(Array(5).keys()).map((_, index) => (
<div <div
key={index} key={index}
+2 -2
View File
@@ -17,8 +17,8 @@ import Section from './widgets/Section';
const Gengar: React.FC<PageProps> = ({ page }) => { const Gengar: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]); const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
+1 -1
View File
@@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<h3 <h3
+5 -5
View File
@@ -16,11 +16,11 @@ import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]); const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
@@ -73,10 +73,10 @@ export const MastheadSidebar: React.FC = () => {
}; };
export const MastheadMain: React.FC = () => { export const MastheadMain: React.FC = () => {
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const backgroundColor: string = useMemo(() => alpha(primaryColor, 0.15), [primaryColor]); const backgroundColor: string = useMemo(() => alpha(primaryColor, 0.15), [primaryColor]);
const { summary } = useAppSelector((state) => state.resume.basics); const { summary } = useAppSelector((state) => state.resume.present.basics);
return ( return (
<div className="grid gap-2 p-4" style={{ backgroundColor }}> <div className="grid gap-2 p-4" style={{ backgroundColor }}>
+7 -4
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,16 +21,18 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Gengar_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
+2 -2
View File
@@ -13,8 +13,8 @@ import Section from './widgets/Section';
const Glalie: React.FC<PageProps> = ({ page }) => { const Glalie: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
return ( return (
<div className={styles.page}> <div className={styles.page}>
@@ -13,7 +13,7 @@ type Props = {
}; };
const BadgeDisplay: React.FC<Props> = ({ items }) => { const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null; if (!isArray(items) || isEmpty(items)) return null;
+1 -1
View File
@@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<h3 <h3
+4 -4
View File
@@ -10,10 +10,10 @@ import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
return ( return (
@@ -65,7 +65,7 @@ export const MastheadSidebar: React.FC = () => {
}; };
export const MastheadMain: React.FC = () => { export const MastheadMain: React.FC = () => {
const { summary } = useAppSelector((state) => state.resume.basics); const { summary } = useAppSelector((state) => state.resume.present.basics);
return <Markdown>{summary}</Markdown>; return <Markdown>{summary}</Markdown>;
}; };
+7 -4
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -21,16 +22,18 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Glalie_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
+2 -2
View File
@@ -12,8 +12,8 @@ import Section from './widgets/Section';
const Kakuna: React.FC<PageProps> = ({ page }) => { const Kakuna: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const { summary } = useAppSelector((state) => state.resume.basics); const { summary } = useAppSelector((state) => state.resume.present.basics);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
return ( return (
<div className={styles.page}> <div className={styles.page}>
@@ -12,7 +12,7 @@ type Props = {
}; };
const BadgeDisplay: React.FC<Props> = ({ items }) => { const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null; if (!isArray(items) || isEmpty(items)) return null;
+3 -3
View File
@@ -10,13 +10,13 @@ import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead = () => { const Masthead = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector( const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
return ( return (
<div className="grid gap-3 justify-center mb-4 border-b pb-4 text-center"> <div className="mb-4 grid justify-center gap-3 border-b pb-4 text-center">
<div className="mx-auto"> <div className="mx-auto">
{photo.visible && !isEmpty(photo.url) && ( {photo.visible && !isEmpty(photo.url) && (
<img <img
+7 -4
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,16 +21,18 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Kakuna_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
+7 -3
View File
@@ -1,7 +1,11 @@
.container { .container {
@apply grid grid-cols-2 gap-8 px-6 py-4; @apply grid grid-cols-2 gap-4 px-6 py-4;
.column { .main {
@apply col-span-1 flex flex-col; @apply grid gap-4;
}
.sidebar {
@apply grid gap-4;
} }
} }
+3 -3
View File
@@ -11,15 +11,15 @@ import Section from './widgets/Section';
const Leafish: React.FC<PageProps> = ({ page }) => { const Leafish: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
return ( return (
<div className={styles.page}> <div className={styles.page}>
{isFirstPage && <Masthead />} {isFirstPage && <Masthead />}
<div className={styles.container}> <div className={styles.container}>
<div className={styles.column}>{layout[0].map((key) => getSectionById(key, Section))}</div> <div className={styles.main}>{layout[0].map((key) => getSectionById(key, Section))}</div>
<div className={styles.column}>{layout[1].map((key) => getSectionById(key, Section))}</div> <div className={styles.sidebar}>{layout[1].map((key) => getSectionById(key, Section))}</div>
</div> </div>
</div> </div>
); );
+2 -2
View File
@@ -4,11 +4,11 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<h2 <h2
className="pb-1 mb-2 font-bold uppercase opacity-75" className="mb-2 pb-1 font-bold uppercase opacity-75"
style={{ borderBottomWidth: '3px', borderColor: theme.primary, color: theme.primary, display: 'inline-block' }} style={{ borderBottomWidth: '3px', borderColor: theme.primary, color: theme.primary, display: 'inline-block' }}
> >
{children} {children}
+7 -17
View File
@@ -11,27 +11,19 @@ import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead: React.FC = () => { const Masthead: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<div> <div>
<div <div className="flex items-center gap-4 p-6" style={{ backgroundColor: alpha(theme.primary, 0.2) }}>
className="flex items-center gap-4 p-6"
id="Masterhead_main"
style={{ backgroundColor: alpha(theme.primary, 0.2) }}
>
<div className="grid flex-1 gap-1"> <div className="grid flex-1 gap-1">
<h1 id="Masterhead_name">{name}</h1> <h1>{name}</h1>
<p style={{ color: theme.primary }} id="Masterhead_headline"> <p style={{ color: theme.primary }}>{headline}</p>
{headline} <p className="opacity-75">{summary}</p>
</p>
<p className="opacity-75" id="Masterhead_summary">
{summary}
</p>
</div> </div>
{photo.visible && !isEmpty(photo.url) && ( {photo.visible && !isEmpty(photo.url) && (
@@ -41,13 +33,11 @@ const Masthead: React.FC = () => {
width={photo.filters.size} width={photo.filters.size}
height={photo.filters.size} height={photo.filters.size}
className={getPhotoClassNames(photo.filters)} className={getPhotoClassNames(photo.filters)}
id="Masterhead_photo"
/> />
)} )}
</div> </div>
<div <div
className="grid gap-y-2 px-8 py-4" className="grid gap-y-2 px-8 py-4"
id="Masterhead_data"
style={{ backgroundColor: alpha(theme.primary, 0.4), gridTemplateColumns: `repeat(2, minmax(0, 1fr))` }} style={{ backgroundColor: alpha(theme.primary, 0.4), gridTemplateColumns: `repeat(2, minmax(0, 1fr))` }}
> >
<DataDisplay icon={<Room />} className="col-span-2"> <DataDisplay icon={<Room />} className="col-span-2">
+14 -19
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,22 +21,23 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section className="mb-4"> <section id={`Leafish_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
className="grid items-start gap-4" className="grid items-start gap-4"
style={{ gridTemplateColumns: `repeat(${section.columns}, minmax(0, 1fr))` }} style={{ gridTemplateColumns: `repeat(${section.columns}, minmax(0, 1fr))` }}
id={`Section_${section.id}`}
> >
{section.items.map((item: ListItem) => { {section.items.map((item: ListItem) => {
const id = item.id, const id = item.id,
@@ -52,16 +54,16 @@ const Section: React.FC<SectionProps> = ({
date = formatDateString(get(item, 'date'), dateFormat); date = formatDateString(get(item, 'date'), dateFormat);
return ( return (
<div key={id} id={id} className={`grid gap-1 mb-2 Section_${section.id}_inner`}> <div key={id} className="mb-2 grid gap-1">
<div> <div className="grid gap-1">
{title && <div className="font-bold Section_title">{title}</div>} {title && <div className="font-bold">{title}</div>}
{subtitle && <div className="Section_subtitle">{subtitle}</div>} {subtitle && <div>{subtitle}</div>}
{date && ( {date && (
<div className="italic text-xs Section_date" style={{ color: primaryColor }}> <div className="text-xs" style={{ color: primaryColor }}>
({date}) ({date})
</div> </div>
)} )}
{headline && <div className="opacity-50 Section_headline">{headline}</div>} {headline && <div className="opacity-50">{headline}</div>}
</div> </div>
{(level || levelNum > 0) && ( {(level || levelNum > 0) && (
@@ -84,14 +86,7 @@ const Section: React.FC<SectionProps> = ({
</div> </div>
)} )}
{summary && ( {summary && <Markdown>{summary}</Markdown>}
<div>
<div className="italic text-xs" style={{ color: primaryColor }}>
Overview
</div>
<Markdown className={`marker:text-[${primaryColor}]`}>{summary}</Markdown>
</div>
)}
{url && ( {url && (
<DataDisplay icon={<Link />} link={url} className="text-xs"> <DataDisplay icon={<Link />} link={url} className="text-xs">
+2 -2
View File
@@ -12,8 +12,8 @@ import Section from './widgets/Section';
const Onyx: React.FC<PageProps> = ({ page }) => { const Onyx: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const { summary } = useAppSelector((state) => state.resume.basics); const { summary } = useAppSelector((state) => state.resume.present.basics);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
return ( return (
<div className={styles.page}> <div className={styles.page}>
+1 -1
View File
@@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}> <h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}>
+2 -2
View File
@@ -9,9 +9,9 @@ import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead: React.FC = () => { const Masthead: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector( const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
return ( return (
+7 -4
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,16 +21,18 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Onyx_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
+1 -1
View File
@@ -10,7 +10,7 @@ import Section from './widgets/Section';
const Pikachu: React.FC<PageProps> = ({ page }) => { const Pikachu: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]); const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
return ( return (
<div className={styles.page}> <div className={styles.page}>
+1 -1
View File
@@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
return ( return (
<h3 <h3
@@ -13,13 +13,13 @@ import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const { name, photo, email, phone, birthdate, website, location, profiles } = useAppSelector( const { name, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.present.basics
); );
return ( return (
<div className="col-span-2 grid justify-items-left gap-4"> <div className="col-span-2 grid justify-items-start gap-4">
{photo.visible && !isEmpty(photo.url) && ( {photo.visible && !isEmpty(photo.url) && (
<img <img
alt={name} alt={name}
@@ -62,10 +62,10 @@ export const MastheadSidebar: React.FC = () => {
}; };
export const MastheadMain: React.FC = () => { export const MastheadMain: React.FC = () => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]); const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const { name, summary, headline } = useAppSelector((state) => state.resume.basics); const { name, summary, headline } = useAppSelector((state) => state.resume.present.basics);
return ( return (
<div <div
+7 -4
View File
@@ -3,6 +3,7 @@ import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isArray from 'lodash/isArray'; import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
@@ -20,16 +21,18 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline', headlinePath = 'headline',
keywordsPath = 'keywords', keywordsPath = 'keywords',
}) => { }) => {
const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
if (!section.visible) return null; if (!section.visible) return null;
if (isArray(section.items) && isEmpty(section.items)) return null; if (isArray(section.items) && isEmpty(section.items)) return null;
return ( return (
<section> <section id={`Pikachu_${sectionId}`}>
<Heading>{section.name}</Heading> <Heading>{section.name}</Heading>
<div <div
+5
View File
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -9,8 +10,12 @@ const DateWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
useEffect(() => { useEffect(() => {
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
// Set Default Timezone to UTC
dayjs.tz.setDefault('UTC');
// Locales // Locales
require('dayjs/locale/ar'); require('dayjs/locale/ar');
require('dayjs/locale/bg'); require('dayjs/locale/bg');
+1 -1
View File
@@ -5,7 +5,7 @@ import { useCallback, useEffect } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => { const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const typography = useAppSelector((state) => get(state.resume, 'metadata.typography')); const typography = useAppSelector((state) => get(state.resume.present, 'metadata.typography'));
const loadFonts = useCallback(async () => { const loadFonts = useCallback(async () => {
const WebFont = (await import('webfontloader')).default; const WebFont = (await import('webfontloader')).default;
+14 -12
View File
@@ -1,10 +1,10 @@
{ {
"name": "reactive-resume", "name": "reactive-resume",
"version": "3.6.1", "version": "3.6.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "env-cmd --silent turbo run dev", "dev": "env-cmd --silent turbo run dev",
"lint": "eslint --fix .", "lint": "turbo run lint",
"build": "env-cmd --silent turbo run build", "build": "env-cmd --silent turbo run build",
"start": "env-cmd --silent turbo run start", "start": "env-cmd --silent turbo run start",
"format": "prettier --write .", "format": "prettier --write .",
@@ -17,20 +17,22 @@
"docs" "docs"
], ],
"dependencies": { "dependencies": {
"turbo": "^1.4.3", "env-cmd": "^10.1.0",
"env-cmd": "^10.1.0" "turbo": "^1.4.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"prettier": "^2.7.1",
"typescript": "^4.7.4",
"standard-version": "^9.5.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-config-prettier": "^8.5.0", "eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-prettier": "^4.2.1",
"@typescript-eslint/parser": "^5.33.1",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.33.1", "prettier": "^2.7.1",
"eslint-plugin-simple-import-sort": "^7.0.0" "standard-version": "^9.5.0",
"typescript": "^4.7.4"
},
"resolutions": {
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2"
} }
} }
+1442 -1835
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -4,6 +4,7 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"typings": "./dist/index.d.ts", "typings": "./dist/index.d.ts",
"scripts": { "scripts": {
"lint": "eslint --fix src",
"dev": "tsc -w", "dev": "tsc -w",
"build": "tsc" "build": "tsc"
}, },
+6 -5
View File
@@ -1,13 +1,14 @@
{ {
"name": "@reactive-resume/server", "name": "@reactive-resume/server",
"scripts": { "scripts": {
"lint": "eslint --fix src",
"dev": "nest start --watch", "dev": "nest start --watch",
"build": "rimraf dist && nest build", "build": "rimraf dist && nest build",
"debug": "nest start --debug --watch", "debug": "nest start --debug --watch",
"start": "node dist/main" "start": "node dist/main"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.154.0", "@aws-sdk/client-s3": "^3.157.0",
"@nestjs/axios": "^0.1.0", "@nestjs/axios": "^0.1.0",
"@nestjs/common": "^9.0.11", "@nestjs/common": "^9.0.11",
"@nestjs/config": "^2.2.0", "@nestjs/config": "^2.2.0",
@@ -28,7 +29,7 @@
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"csvtojson": "^2.0.10", "csvtojson": "^2.0.10",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"google-auth-library": "^8.3.0", "google-auth-library": "^8.4.0",
"joi": "^17.6.0", "joi": "^17.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
@@ -39,8 +40,8 @@
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.7.3", "pg": "^8.8.0",
"playwright-chromium": "^1.25.0", "playwright-chromium": "^1.25.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",
@@ -56,7 +57,7 @@
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.184",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.7.9", "@types/node": "^18.7.13",
"@types/nodemailer": "^6.4.5", "@types/nodemailer": "^6.4.5",
"@types/passport-jwt": "^3.0.6", "@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34", "@types/passport-local": "^1.0.34",
+3
View File
@@ -4,6 +4,9 @@
"dev": { "dev": {
"cache": false "cache": false
}, },
"lint": {
"cache": false
},
"start": { "start": {
"cache": false "cache": false
}, },