feat: additional work sections

This commit is contained in:
Alexander KIRILOV
2022-10-27 15:43:15 +02:00
parent 9b1f3eda05
commit 7bc4a998fe
13 changed files with 184 additions and 38 deletions

5
.gitignore vendored
View File

@ -10,4 +10,7 @@ node_modules
.DS_Store .DS_Store
# Turbo # Turbo
.turbo .turbo
# Intellij
.idea

View File

@ -1,14 +1,15 @@
import { Add, Star } from '@mui/icons-material'; import { Add, Star } from '@mui/icons-material';
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material'; import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
import { Section as SectionRecord } from '@reactive-resume/schema'; import { Section as SectionRecord } from '@reactive-resume/schema';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get'; import get from 'lodash/get';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMemo } from 'react'; import React, { ReactComponentElement, useMemo } from 'react';
import { validate } from 'uuid'; import { validate } from 'uuid';
import Logo from '@/components/shared/Logo'; import Logo from '@/components/shared/Logo';
import { getCustomSections, left } from '@/config/sections'; import { getCustomSections, getSectionsByType, left } from '@/config/sections';
import { setSidebarState } from '@/store/build/buildSlice'; import { setSidebarState } from '@/store/build/buildSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addSection } from '@/store/resume/resumeSlice'; import { addSection } from '@/store/resume/resumeSlice';
@ -52,7 +53,49 @@ const LeftSidebar = () => {
items: [], items: [],
}; };
dispatch(addSection({ value: newSection })); dispatch(addSection({ value: newSection, type: 'custom' }));
};
const sectionsList = () => {
const sectionsComponents: Array<ReactComponentElement<any>> = [];
for (const item of left) {
const id = (item as any).id;
const component = (item as any).component;
const type = component.props.type || 'basic';
const addMore = !!component.props.addMore;
sectionsComponents.push(
<section key={id} id={id}>
{component}
</section>
);
if (addMore) {
const additionalSections = getSectionsByType(sections, type);
const elements = [];
for (const element of additionalSections) {
const newId = element.id;
const props = cloneDeep(component.props);
props.path = 'sections.' + newId;
props.name = element.name;
props.isDeletable = true;
props.addMore = false;
props.isDuplicated = true;
const newComponent = React.cloneElement(component, props);
elements.push(
<section key={newId} id={`section-${newId}`}>
{newComponent}
</section>
);
}
sectionsComponents.push(...elements);
}
}
return sectionsComponents;
}; };
return ( return (
@ -100,12 +143,7 @@ const LeftSidebar = () => {
</nav> </nav>
<main> <main>
{left.map(({ id, component }) => ( {sectionsList()}
<section key={id} id={id}>
{component}
</section>
))}
{customSections.map(({ id }) => ( {customSections.map(({ id }) => (
<section key={id} id={`section-${id}`}> <section key={id} id={`section-${id}`}>
<Section path={`sections.${id}`} isEditable isHideable isDeletable /> <Section path={`sections.${id}`} isEditable isHideable isDeletable />

View File

@ -1,6 +1,6 @@
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { ListItem } from '@reactive-resume/schema'; import { ListItem, Section as SectionRecord, SectionType } from '@reactive-resume/schema';
import clsx from 'clsx'; import clsx from 'clsx';
import get from 'lodash/get'; import get from 'lodash/get';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@ -10,28 +10,34 @@ import Heading from '@/components/shared/Heading';
import List from '@/components/shared/List'; import List from '@/components/shared/List';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { ModalName, setModalState } from '@/store/modal/modalSlice'; import { ModalName, setModalState } from '@/store/modal/modalSlice';
import { duplicateItem } from '@/store/resume/resumeSlice'; import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
import SectionSettings from './SectionSettings'; import SectionSettings from './SectionSettings';
type Props = { type Props = {
path: `sections.${string}`; path: `sections.${string}`;
type?: SectionType;
name?: string; name?: string;
titleKey?: string; titleKey?: string;
subtitleKey?: string; subtitleKey?: string;
isEditable?: boolean; isEditable?: boolean;
isHideable?: boolean; isHideable?: boolean;
isDeletable?: boolean; isDeletable?: boolean;
addMore?: boolean;
isDuplicated?: boolean;
}; };
const Section: React.FC<Props> = ({ const Section: React.FC<Props> = ({
path, path,
name = 'Section Name', name = 'Section Name',
type = 'basic',
titleKey = 'title', titleKey = 'title',
subtitleKey = 'subtitle', subtitleKey = 'subtitle',
isEditable = false, isEditable = false,
isHideable = false, isHideable = false,
isDeletable = false, isDeletable = false,
addMore = false,
isDuplicated = false,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -42,21 +48,43 @@ const Section: React.FC<Props> = ({
const handleAdd = () => { const handleAdd = () => {
const id = path.split('.')[1]; const id = path.split('.')[1];
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`; let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
if (type) {
modal = `builder.sections.${type}`;
}
dispatch(setModalState({ modal, state: { open: true, payload: { path } } })); dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
}; };
const handleEdit = (item: ListItem) => { const handleEdit = (item: ListItem) => {
const id = path.split('.')[1]; const id = path.split('.')[1];
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`; let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
const payload = validate(id) ? { path, item } : { item }; const payload = validate(id) ? { path, item } : { item };
if (isDuplicated) {
modal = `builder.sections.${type}`;
payload.path = path;
}
dispatch(setModalState({ modal, state: { open: true, payload } })); dispatch(setModalState({ modal, state: { open: true, payload } }));
}; };
const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item })); const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item }));
const handleDuplicateSection = () => {
const newSection: SectionRecord = {
name: `${heading}`,
type: type,
visible: true,
columns: 2,
items: [],
isDuplicated: true
};
dispatch(duplicateSection({ value: newSection, type }));
};
return ( return (
<> <>
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} /> <Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
@ -77,6 +105,16 @@ const Section: React.FC<Props> = ({
{t<string>('builder.common.actions.add', { token: heading })} {t<string>('builder.common.actions.add', { token: heading })}
</Button> </Button>
</footer> </footer>
{addMore ? (
<div className="py-6 text-right">
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleDuplicateSection}>
{t<string>('builder.common.actions.duplicate')}
</Button>
</div>
) : (
<></>
)}
</> </>
); );
}; };

View File

@ -23,7 +23,7 @@ import {
VolunteerActivism, VolunteerActivism,
Work, Work,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { Section as SectionRecord } from '@reactive-resume/schema'; import { Section as SectionRecord, SectionType } from '@reactive-resume/schema';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import Basics from '@/components/build/LeftSidebar/sections/Basics'; import Basics from '@/components/build/LeftSidebar/sections/Basics';
@ -60,59 +60,69 @@ export const left: SidebarSection[] = [
{ {
id: 'work', id: 'work',
icon: <Work />, icon: <Work />,
component: <Section path="sections.work" titleKey="name" subtitleKey="position" isEditable isHideable />, component: (
<Section
type={'work'}
addMore={true}
path="sections.work"
titleKey="name"
subtitleKey="position"
isEditable
isHideable
/>
),
}, },
{ {
id: 'education', id: 'education',
icon: <School />, icon: <School />,
component: <Section path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />, component: <Section type={"education"} path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />,
}, },
{ {
id: 'awards', id: 'awards',
icon: <EmojiEvents />, icon: <EmojiEvents />,
component: <Section path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />, component: <Section type={"awards"} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />,
}, },
{ {
id: 'certifications', id: 'certifications',
icon: <CardGiftcard />, icon: <CardGiftcard />,
component: <Section path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />, component: <Section type={"certifications"} path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />,
}, },
{ {
id: 'publications', id: 'publications',
icon: <MenuBook />, icon: <MenuBook />,
component: <Section path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />, component: <Section type={"publications"} path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />,
}, },
{ {
id: 'skills', id: 'skills',
icon: <Architecture />, icon: <Architecture />,
component: <Section path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />, component: <Section type={"skills"} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />,
}, },
{ {
id: 'languages', id: 'languages',
icon: <Language />, icon: <Language />,
component: <Section path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />, component: <Section type={"languages"} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />,
}, },
{ {
id: 'interests', id: 'interests',
icon: <Sailing />, icon: <Sailing />,
component: <Section path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />, component: <Section type={"interests"} path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />,
}, },
{ {
id: 'volunteer', id: 'volunteer',
icon: <VolunteerActivism />, icon: <VolunteerActivism />,
component: ( component: (
<Section path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable /> <Section type={"volunteer"} path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable />
), ),
}, },
{ {
id: 'projects', id: 'projects',
icon: <Coffee />, icon: <Coffee />,
component: <Section path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />, component: <Section type={"projects"} path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />,
}, },
{ {
id: 'references', id: 'references',
icon: <Groups />, icon: <Groups />,
component: <Section path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />, component: <Section type={"references"} path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />,
}, },
]; ];
@ -164,6 +174,21 @@ export const right: SidebarSection[] = [
}, },
]; ];
export const getSectionsByType = (
sections: Record<string, SectionRecord>,
type: SectionType
): Array<Required<SectionRecord>> => {
if (isEmpty(sections)) return [];
return Object.entries(sections).reduce((acc, [id, section]) => {
if (section.type.startsWith(type) && section.isDuplicated) {
return [...acc, { ...section, id }];
}
return acc;
}, [] as Array<Required<SectionRecord>>);
};
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => { export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => {
if (isEmpty(sections)) return []; if (isEmpty(sections)) return [];

View File

@ -169,7 +169,8 @@ const LoginModal: React.FC = () => {
<p className="text-xs"> <p className="text-xs">
<Trans t={t} i18nKey="modals.auth.login.recover-text"> <Trans t={t} i18nKey="modals.auth.login.recover-text">
In case you have forgotten your password, you can <a onClick={handleRecoverAccount}>recover your account here.</a> In case you have forgotten your password, you can{' '}
<a onClick={handleRecoverAccount}>recover your account here.</a>
</Trans> </Trans>
</p> </p>
</BaseModal> </BaseModal>

View File

@ -2,7 +2,7 @@ import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material'; import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers'; import { DatePicker } from '@mui/x-date-pickers';
import { SectionPath, WorkExperience } from '@reactive-resume/schema'; import { WorkExperience } from '@reactive-resume/schema';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Joi from 'joi'; import Joi from 'joi';
import get from 'lodash/get'; import get from 'lodash/get';
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = WorkExperience; type FormData = WorkExperience;
const path: SectionPath = 'sections.work';
const defaultState: FormData = { const defaultState: FormData = {
name: '', name: '',
position: '', position: '',
@ -51,9 +49,11 @@ const WorkModal: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume.present, `${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.sections.work']);
const path: string = get(payload, 'path', 'sections.work');
const item: FormData = get(payload, 'item', null); const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]); const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]); const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
@ -77,7 +77,7 @@ const WorkModal: React.FC = () => {
const handleClose = () => { const handleClose = () => {
dispatch( dispatch(
setModalState({ setModalState({
modal: `builder.${path}`, modal: 'builder.sections.work',
state: { open: false }, state: { open: false },
}) })
); );

View File

@ -3,7 +3,8 @@
"actions": { "actions": {
"add": "Add New {{token}}", "add": "Add New {{token}}",
"delete": "Delete {{token}}", "delete": "Delete {{token}}",
"edit": "Edit {{token}}" "edit": "Edit {{token}}",
"duplicate": "Duplicate Section"
}, },
"columns": { "columns": {
"heading": "Columns", "heading": "Columns",

View File

@ -9,6 +9,7 @@ export type ModalName =
| 'dashboard.import-external' | 'dashboard.import-external'
| 'dashboard.rename-resume' | 'dashboard.rename-resume'
| 'builder.sections.profile' | 'builder.sections.profile'
| 'builder.sections.work'
| `builder.sections.${string}`; | `builder.sections.${string}`;
export type ModalState = { export type ModalState = {

View File

@ -1,4 +1,4 @@
import { ListItem, Profile, Resume, Section } from '@reactive-resume/schema'; import { ListItem, Profile, Resume, Section, SectionType } from '@reactive-resume/schema';
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get'; import get from 'lodash/get';
@ -7,6 +7,8 @@ import pick from 'lodash/pick';
import set from 'lodash/set'; import set from 'lodash/set';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { getSectionsByType } from '@/config/sections';
type SetResumeStatePayload = { path: string; value: unknown }; type SetResumeStatePayload = { path: string; value: unknown };
type AddItemPayload = { path: string; value: ListItem }; type AddItemPayload = { path: string; value: ListItem };
@ -17,7 +19,7 @@ type DuplicateItemPayload = { path: string; value: ListItem };
type DeleteItemPayload = { path: string; value: ListItem }; type DeleteItemPayload = { path: string; value: ListItem };
type AddSectionPayload = { value: Section }; type AddSectionPayload = { value: Section; type: SectionType };
type DeleteSectionPayload = { path: string }; type DeleteSectionPayload = { path: string };
@ -80,6 +82,15 @@ export const resumeSlice = createSlice({
state.sections[id] = value; state.sections[id] = value;
state.metadata.layout[0][0].push(id); state.metadata.layout[0][0].push(id);
}, },
duplicateSection: (state: Resume, action: PayloadAction<AddSectionPayload>) => {
const { value, type } = action.payload;
const id = getSectionsByType(state.sections, type).length + 1;
value.name = value.name + '-' + id;
state.sections[`${type}-${id}`] = value;
state.metadata.layout[0][0].push(`${type}-${id}`);
},
deleteSection: (state: Resume, action: PayloadAction<DeleteSectionPayload>) => { deleteSection: (state: Resume, action: PayloadAction<DeleteSectionPayload>) => {
const { path } = action.payload; const { path } = action.payload;
const id = path.split('.')[1]; const id = path.split('.')[1];
@ -119,6 +130,7 @@ export const {
duplicateItem, duplicateItem,
deleteItem, deleteItem,
addSection, addSection,
duplicateSection,
deleteSection, deleteSection,
addPage, addPage,
deletePage, deletePage,

View File

@ -1,3 +1,4 @@
import { find } from 'lodash';
import get from 'lodash/get'; import get from 'lodash/get';
import React from 'react'; import React from 'react';
import { validate } from 'uuid'; import { validate } from 'uuid';
@ -44,11 +45,21 @@ const sectionMap = (Section: React.FC<SectionProps>): Record<string, JSX.Element
}); });
export const getSectionById = (id: string, Section: React.FC<SectionProps>): JSX.Element => { export const getSectionById = (id: string, Section: React.FC<SectionProps>): JSX.Element => {
// Check if section id is a custom section (an uuid)
if (validate(id)) { if (validate(id)) {
return <Section key={id} path={`sections.${id}`} />; return <Section key={id} path={`sections.${id}`} />;
} }
return get(sectionMap(Section), id); // Check if section id is a predefined seciton in config
const predefinedSection = get(sectionMap(Section), id);
if(predefinedSection) {
return predefinedSection;
}
// Other ways section should be a cloned section
const section = find(sectionMap(Section), (element, key) => id.includes(key));
return React.cloneElement(section!, { path: `sections.${id}` });
}; };
export default sectionMap; export default sectionMap;

View File

@ -125,7 +125,22 @@ export type ListItem =
| WorkExperience | WorkExperience
| Custom; | Custom;
export type SectionType = 'basic' | 'custom'; export type SectionType =
| 'basic'
| 'location'
| 'profiles'
| 'education'
| 'awards'
| 'certifications'
| 'publications'
| 'skills'
| 'languages'
| 'interests'
| 'volunteer'
| 'projects'
| 'references'
| 'custom'
| 'work';
export type SectionPath = `sections.${string}`; export type SectionPath = `sections.${string}`;
@ -136,4 +151,5 @@ export type Section = {
columns: number; columns: number;
visible: boolean; visible: boolean;
items: ListItem[]; items: ListItem[];
isDuplicated: boolean;
}; };

View File

@ -38,7 +38,7 @@ const defaultState: Partial<Resume> = {
work: { work: {
id: 'work', id: 'work',
name: 'Work Experience', name: 'Work Experience',
type: 'basic', type: 'work',
columns: 2, columns: 2,
visible: true, visible: true,
items: [], items: [],