🚀 release v3.0.0

This commit is contained in:
Amruth Pillai
2022-03-06 22:48:29 +01:00
parent 00505a9e5d
commit 9c1380f401
373 changed files with 12050 additions and 15783 deletions

View File

@ -0,0 +1,185 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { Award, SectionPath } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Award;
const path: SectionPath = 'sections.awards';
const defaultState: FormData = {
title: '',
awarder: '',
date: '',
url: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
title: Joi.string().required(),
awarder: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
});
const AwardModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="title"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.title.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="awarder"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.leftSidebar.sections.awards.form.awarder.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default AwardModal;

View File

@ -0,0 +1,185 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { Certificate, SectionPath } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Certificate;
const path: SectionPath = 'sections.certifications';
const defaultState: FormData = {
name: '',
issuer: '',
date: '',
url: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
issuer: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
});
const CertificateModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="issuer"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.leftSidebar.sections.certifications.form.issuer.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default CertificateModal;

View File

@ -0,0 +1,290 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, Slider, TextField } from '@mui/material';
import { Custom } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Custom;
export type CustomModalPayload = {
path: string;
item?: Custom;
};
const defaultState: FormData = {
title: '',
subtitle: '',
date: {
start: '',
end: '',
},
url: '',
level: '',
levelNum: 0,
summary: '',
keywords: [],
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
title: Joi.string().required(),
subtitle: Joi.string().allow(''),
date: Joi.object().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
level: Joi.string().allow(''),
levelNum: Joi.number().min(0).max(10),
summary: Joi.string().allow(''),
keywords: Joi.array().items(Joi.string().optional()),
});
const CustomModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
const path: string = get(payload, 'path', '');
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: 'builder.sections.custom',
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="title"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.title.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="subtitle"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.subtitle.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date.start"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.start-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="date.end"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.end-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="level"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.level.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="levelNum"
control={control}
render={({ field }) => (
<div className="col-span-2">
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
<div className="px-10">
<Slider
{...field}
marks={[
{
value: 0,
label: 'Disable',
},
{
value: 1,
label: 'Beginner',
},
{
value: 10,
label: 'Expert',
},
]}
min={0}
max={10}
defaultValue={0}
color="secondary"
valueLabelDisplay="auto"
aria-label={t('builder.common.form.levelNum.label')}
/>
</div>
</div>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
<Controller
name="keywords"
control={control}
render={({ field, fieldState }) => (
<ArrayInput
label={t('builder.common.form.keywords.label')}
value={field.value as string[]}
onChange={field.onChange}
errors={fieldState.error}
className="col-span-2"
/>
)}
/>
</form>
</BaseModal>
);
};
export default CustomModal;

View File

@ -0,0 +1,263 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { Education, SectionPath } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Education;
const path: SectionPath = 'sections.education';
const defaultState: FormData = {
institution: '',
degree: '',
area: '',
score: '',
date: {
start: '',
end: '',
},
url: '',
summary: '',
courses: [],
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
institution: Joi.string().required(),
degree: Joi.string().required(),
area: Joi.string().allow(''),
score: Joi.string().allow(''),
date: Joi.object().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
courses: Joi.array().items(Joi.string().optional()),
});
const EducationModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="institution"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.leftSidebar.sections.education.form.institution.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="degree"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.leftSidebar.sections.education.form.degree.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="area"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.leftSidebar.sections.education.form.area-study.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="score"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.leftSidebar.sections.education.form.grade.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date.start"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.start-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="date.end"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.end-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
<Controller
name="courses"
control={control}
render={({ field, fieldState }) => (
<ArrayInput
label={t('builder.leftSidebar.sections.education.form.courses.label')}
value={field.value as string[]}
onChange={field.onChange}
errors={fieldState.error}
className="col-span-2"
/>
)}
/>
</form>
</BaseModal>
);
};
export default EducationModal;

View File

@ -0,0 +1,122 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, TextField } from '@mui/material';
import { Interest, SectionPath } from '@reactive-resume/schema';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Interest;
const path: SectionPath = 'sections.interests';
const defaultState: FormData = {
name: '',
keywords: [],
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
keywords: Joi.array().items(Joi.string().optional()),
});
const InterestModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="keywords"
control={control}
render={({ field, fieldState }) => (
<ArrayInput
label={t('builder.common.form.keywords.label')}
value={field.value as string[]}
onChange={field.onChange}
errors={fieldState.error}
className="col-span-2"
/>
)}
/>
</form>
</BaseModal>
);
};
export default InterestModal;

View File

@ -0,0 +1,158 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, Slider, TextField } from '@mui/material';
import { Language, SectionPath } from '@reactive-resume/schema';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Language;
const path: SectionPath = 'sections.languages';
const defaultState: FormData = {
name: '',
level: '',
levelNum: 0,
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
level: Joi.string().required(),
levelNum: Joi.number().min(0).max(10).required(),
});
const LanguageModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="level"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.common.form.level.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="levelNum"
control={control}
render={({ field }) => (
<div className="col-span-2">
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
<div className="px-10">
<Slider
{...field}
marks={[
{
value: 0,
label: 'Disable',
},
{
value: 1,
label: 'Beginner',
},
{
value: 10,
label: 'Expert',
},
]}
min={0}
max={10}
defaultValue={0}
color="secondary"
valueLabelDisplay="auto"
aria-label={t('builder.common.form.levelNum.label')}
/>
</div>
</div>
)}
/>
</form>
</BaseModal>
);
};
export default LanguageModal;

View File

@ -0,0 +1,145 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, AlternateEmail, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, TextField } from '@mui/material';
import { Profile } from '@reactive-resume/schema';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Profile;
const path = 'sections.profile';
const defaultState: FormData = {
network: '',
username: '',
url: 'https://',
};
const schema = Joi.object<FormData>({
id: Joi.string(),
network: Joi.string().required(),
username: Joi.string().required(),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.default('https://')
.allow(''),
});
const ProfileModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = t('builder.common.actions.add', {
section: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
});
const editText = t('builder.common.actions.edit', {
section: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
});
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: 'basics.profiles', value: formData }));
} else {
dispatch(addItem({ path: 'basics.profiles', value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
heading={isEditMode ? editText : addText}
handleClose={handleClose}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="network"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.leftSidebar.sections.profiles.form.network.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="username"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.leftSidebar.sections.profiles.form.username.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
InputProps={{
startAdornment: <AlternateEmail className="mr-2" />,
}}
{...field}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default ProfileModal;

View File

@ -0,0 +1,233 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { Project, SectionPath } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Project;
const path: SectionPath = 'sections.projects';
const defaultState: FormData = {
name: '',
description: '',
date: {
start: '',
end: '',
},
url: '',
summary: '',
keywords: [],
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
description: Joi.string().required(),
date: Joi.object().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
keywords: Joi.array().items(Joi.string().optional()),
});
const ProjectModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.common.form.description.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date.start"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.start-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="date.end"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.end-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
<Controller
name="keywords"
control={control}
render={({ field, fieldState }) => (
<ArrayInput
label={t('builder.common.form.keywords.label')}
value={field.value as string[]}
onChange={field.onChange}
errors={fieldState.error}
className="col-span-2"
/>
)}
/>
</form>
</BaseModal>
);
};
export default ProjectModal;

View File

@ -0,0 +1,185 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { Publication, SectionPath } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Publication;
const path: SectionPath = 'sections.publications';
const defaultState: FormData = {
name: '',
publisher: '',
date: '',
url: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
publisher: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
});
const PublicationModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="publisher"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label="{t('builder.leftSidebar.sections.publications.form.publisher.label')}"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default PublicationModal;

View File

@ -0,0 +1,170 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, TextField } from '@mui/material';
import { Reference, SectionPath } from '@reactive-resume/schema';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Reference;
const path: SectionPath = 'sections.references';
const defaultState: FormData = {
name: '',
relationship: '',
phone: '',
email: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
relationship: Joi.string().required(),
phone: Joi.string().allow(''),
email: Joi.string().allow(''),
summary: Joi.string().allow(''),
});
const ReferenceModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="relationship"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.leftSidebar.sections.references.form.relationship.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="phone"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.phone.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="email"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.email.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default ReferenceModal;

View File

@ -0,0 +1,175 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, Slider, TextField } from '@mui/material';
import { SectionPath, Skill } from '@reactive-resume/schema';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Skill;
const path: SectionPath = 'sections.skills';
const defaultState: FormData = {
name: '',
level: '',
levelNum: 0,
keywords: [],
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
level: Joi.string().required(),
levelNum: Joi.number().min(0).max(10).required(),
keywords: Joi.array().items(Joi.string().optional()),
});
const SkillModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="level"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.common.form.level.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="levelNum"
control={control}
render={({ field }) => (
<div className="col-span-2">
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
<div className="px-10">
<Slider
{...field}
marks={[
{
value: 0,
label: 'Disable',
},
{
value: 1,
label: 'Beginner',
},
{
value: 10,
label: 'Expert',
},
]}
min={0}
max={10}
defaultValue={0}
color="secondary"
valueLabelDisplay="auto"
aria-label={t('builder.common.form.levelNum.label')}
/>
</div>
</div>
)}
/>
<Controller
name="keywords"
control={control}
render={({ field, fieldState }) => (
<ArrayInput
label={t('builder.common.form.keywords.label')}
value={field.value as string[]}
onChange={field.onChange}
errors={fieldState.error}
className="col-span-2"
/>
)}
/>
</form>
</BaseModal>
);
};
export default SkillModal;

View File

@ -0,0 +1,216 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { SectionPath, Volunteer } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = Volunteer;
const path: SectionPath = 'sections.volunteer';
const defaultState: FormData = {
organization: '',
position: '',
date: {
start: '',
end: '',
},
url: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
organization: Joi.string().required(),
position: Joi.string().required(),
date: Joi.object().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
});
const VolunteerModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="organization"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.leftSidebar.sections.volunteer.form.organization.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="position"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.common.form.position.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date.start"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.start-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="date.end"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.end-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default VolunteerModal;

View File

@ -0,0 +1,216 @@
import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import DatePicker from '@mui/lab/DatePicker';
import { Button, TextField } from '@mui/material';
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = WorkExperience;
const path: SectionPath = 'sections.work';
const defaultState: FormData = {
name: '',
position: '',
date: {
start: '',
end: '',
},
url: '',
summary: '',
};
const schema = Joi.object<FormData>().keys({
id: Joi.string(),
name: Joi.string().required(),
position: Joi.string().required(),
date: Joi.object().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
summary: Joi.string().allow(''),
});
const WorkModal: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
const { reset, control, handleSubmit } = useForm<FormData>({
defaultValues: defaultState,
resolver: joiResolver(schema),
});
const onSubmit = (formData: FormData) => {
if (isEditMode) {
dispatch(editItem({ path: `${path}.items`, value: formData }));
} else {
dispatch(addItem({ path: `${path}.items`, value: formData }));
}
handleClose();
};
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
state: { open: false },
})
);
reset(defaultState);
};
useEffect(() => {
if (!isEmpty(item)) {
reset(item);
}
}, [item, reset]);
return (
<BaseModal
icon={isEditMode ? <DriveFileRenameOutline /> : <Add />}
isOpen={isOpen}
handleClose={handleClose}
heading={isEditMode ? editText : addText}
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
>
<form className="my-2 grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
required
autoFocus
label={t('builder.common.form.name.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="position"
control={control}
render={({ field, fieldState }) => (
<TextField
required
label={t('builder.common.form.position.label')}
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="date.start"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.start-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || params.inputProps?.placeholder}
/>
)}
/>
)}
/>
<Controller
name="date.end"
control={control}
render={({ field, fieldState }) => (
<DatePicker
{...field}
openTo="year"
label={t('builder.common.form.end-date.label')}
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).isValid() && field.onChange(date.toISOString());
}}
renderInput={(params) => (
<TextField
{...params}
error={!!fieldState.error}
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
/>
)}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
/>
)}
/>
<Controller
name="summary"
control={control}
render={({ field, fieldState }) => (
<TextField
multiline
minRows={3}
maxRows={6}
label={t('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>
)}
/>
</form>
</BaseModal>
);
};
export default WorkModal;