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

3
.gitignore vendored
View File

@ -11,3 +11,6 @@ node_modules
# Turbo
.turbo
# Intellij
.idea

View File

@ -1,14 +1,15 @@
import { Add, Star } from '@mui/icons-material';
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
import { Section as SectionRecord } from '@reactive-resume/schema';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import { useMemo } from 'react';
import React, { ReactComponentElement, useMemo } from 'react';
import { validate } from 'uuid';
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 { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addSection } from '@/store/resume/resumeSlice';
@ -52,7 +53,49 @@ const LeftSidebar = () => {
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 (
@ -100,12 +143,7 @@ const LeftSidebar = () => {
</nav>
<main>
{left.map(({ id, component }) => (
<section key={id} id={id}>
{component}
</section>
))}
{sectionsList()}
{customSections.map(({ id }) => (
<section key={id} id={`section-${id}`}>
<Section path={`sections.${id}`} isEditable isHideable isDeletable />

View File

@ -1,6 +1,6 @@
import { Add } from '@mui/icons-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 get from 'lodash/get';
import { useTranslation } from 'next-i18next';
@ -10,28 +10,34 @@ import Heading from '@/components/shared/Heading';
import List from '@/components/shared/List';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { ModalName, setModalState } from '@/store/modal/modalSlice';
import { duplicateItem } from '@/store/resume/resumeSlice';
import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
import SectionSettings from './SectionSettings';
type Props = {
path: `sections.${string}`;
type?: SectionType;
name?: string;
titleKey?: string;
subtitleKey?: string;
isEditable?: boolean;
isHideable?: boolean;
isDeletable?: boolean;
addMore?: boolean;
isDuplicated?: boolean;
};
const Section: React.FC<Props> = ({
path,
name = 'Section Name',
type = 'basic',
titleKey = 'title',
subtitleKey = 'subtitle',
isEditable = false,
isHideable = false,
isDeletable = false,
addMore = false,
isDuplicated = false,
}) => {
const { t } = useTranslation();
@ -42,21 +48,43 @@ const Section: React.FC<Props> = ({
const handleAdd = () => {
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 } } }));
};
const handleEdit = (item: ListItem) => {
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 };
if (isDuplicated) {
modal = `builder.sections.${type}`;
payload.path = path;
}
dispatch(setModalState({ modal, state: { open: true, payload } }));
};
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 (
<>
<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 })}
</Button>
</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,
Work,
} 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 Basics from '@/components/build/LeftSidebar/sections/Basics';
@ -60,59 +60,69 @@ export const left: SidebarSection[] = [
{
id: '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',
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',
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',
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',
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',
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',
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',
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',
icon: <VolunteerActivism />,
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',
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',
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>> => {
if (isEmpty(sections)) return [];

View File

@ -169,7 +169,8 @@ const LoginModal: React.FC = () => {
<p className="text-xs">
<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>
</p>
</BaseModal>

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export type ModalName =
| 'dashboard.import-external'
| 'dashboard.rename-resume'
| 'builder.sections.profile'
| 'builder.sections.work'
| `builder.sections.${string}`;
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 cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
@ -7,6 +7,8 @@ import pick from 'lodash/pick';
import set from 'lodash/set';
import { v4 as uuidv4 } from 'uuid';
import { getSectionsByType } from '@/config/sections';
type SetResumeStatePayload = { path: string; value: unknown };
type AddItemPayload = { path: string; value: ListItem };
@ -17,7 +19,7 @@ type DuplicateItemPayload = { path: string; value: ListItem };
type DeleteItemPayload = { path: string; value: ListItem };
type AddSectionPayload = { value: Section };
type AddSectionPayload = { value: Section; type: SectionType };
type DeleteSectionPayload = { path: string };
@ -80,6 +82,15 @@ export const resumeSlice = createSlice({
state.sections[id] = value;
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>) => {
const { path } = action.payload;
const id = path.split('.')[1];
@ -119,6 +130,7 @@ export const {
duplicateItem,
deleteItem,
addSection,
duplicateSection,
deleteSection,
addPage,
deletePage,

View File

@ -1,3 +1,4 @@
import { find } from 'lodash';
import get from 'lodash/get';
import React from 'react';
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 => {
// Check if section id is a custom section (an uuid)
if (validate(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;

View File

@ -125,7 +125,22 @@ export type ListItem =
| WorkExperience
| 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}`;
@ -136,4 +151,5 @@ export type Section = {
columns: number;
visible: boolean;
items: ListItem[];
isDuplicated: boolean;
};

View File

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