mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 01:44:53 +10:00
feat: ⚡ update app version to 3.6.2
This commit is contained in:
+10
-7
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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}>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,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,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>
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1442
-1835
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"dev": {
|
"dev": {
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
"lint": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user