mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-12 05:55:02 +10:00
- implement work experience
- implement education - show dynamic names in layout
This commit is contained in:
@@ -12,9 +12,9 @@ const LeftSidebar = () => {
|
||||
<LeftNavbar />
|
||||
|
||||
<div className={styles.container}>
|
||||
{sections.map(({ id, component: Component }) => (
|
||||
{sections.map(({ id, name, event, component: Component }) => (
|
||||
<Fragment key={id}>
|
||||
<Component state={state} />
|
||||
<Component id={id} name={name} event={event} state={state} />
|
||||
<hr />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
.container {
|
||||
z-index: 10;
|
||||
box-shadow: var(--left-shadow);
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
@apply w-full h-screen overflow-scroll p-8;
|
||||
@apply grid gap-6;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const EmptyList = () => (
|
||||
<div className="rounded border border-secondary py-6 text-center">
|
||||
This list is empty.
|
||||
</div>
|
||||
<div className="py-6 opacity-75 text-center">This list is empty.</div>
|
||||
);
|
||||
|
||||
export default EmptyList;
|
||||
|
||||
@@ -1,22 +1,67 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import React from "react";
|
||||
import { get, isEmpty } from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useContext } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import ModalContext from "../../../contexts/ModalContext";
|
||||
import Button from "../../shared/Button";
|
||||
import EmptyList from "./EmptyList";
|
||||
import styles from "./List.module.css";
|
||||
|
||||
const List = ({ items, onAdd, children }) => {
|
||||
const List = ({
|
||||
path,
|
||||
items,
|
||||
title,
|
||||
titlePath,
|
||||
subtitle,
|
||||
subtitlePath,
|
||||
text,
|
||||
textPath,
|
||||
event,
|
||||
listItemComponent: ListItemComponent,
|
||||
}) => {
|
||||
const { emitter } = useContext(ModalContext);
|
||||
|
||||
const handleAdd = () => emitter.emit(event);
|
||||
|
||||
const handleEdit = (data) => emitter.emit(event, data);
|
||||
|
||||
const formatDateRange = (x) =>
|
||||
`${moment(x.startDate).format("MMMM Y")} — ${
|
||||
moment(x.endDate).isValid()
|
||||
? moment(x.endDate).format("MMMM Y")
|
||||
: "Present"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={styles.container}>
|
||||
{isEmpty(items) ? <EmptyList /> : children}
|
||||
<div className={styles.list}>
|
||||
{isEmpty(items) ? (
|
||||
<EmptyList />
|
||||
) : (
|
||||
items.map((x, i) => (
|
||||
<ListItemComponent
|
||||
key={x.id}
|
||||
data={x}
|
||||
path={path}
|
||||
title={title || get(x, titlePath, "")}
|
||||
subtitle={
|
||||
subtitle || get(x, subtitlePath, "") || formatDateRange(x)
|
||||
}
|
||||
text={text || get(x, textPath, "")}
|
||||
className={styles.listItem}
|
||||
onEdit={() => handleEdit(x)}
|
||||
isFirst={i === 0}
|
||||
isLast={i === items.length - 1}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
outline
|
||||
icon={MdAdd}
|
||||
title="Add New"
|
||||
onClick={onAdd}
|
||||
onClick={handleAdd}
|
||||
className="mt-8 ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
.container {
|
||||
.list {
|
||||
@apply flex flex-col border border-secondary rounded;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
@apply flex items-center justify-between border-t border-secondary px-8 py-5;
|
||||
}
|
||||
|
||||
.list-item:first-child {
|
||||
@apply border-t-0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@apply opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.list-item:hover .menu {
|
||||
@apply opacity-100;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
+3
-3
@@ -3,9 +3,9 @@ import React, { useContext, useState } from "react";
|
||||
import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io";
|
||||
import { MdMoreVert } from "react-icons/md";
|
||||
import ResumeContext from "../../../../contexts/ResumeContext";
|
||||
import styles from "./SmallListItem.module.css";
|
||||
import styles from "./DoubleFieldListItem.module.css";
|
||||
|
||||
const SmallListItem = ({
|
||||
const DoubleFieldListItem = ({
|
||||
title,
|
||||
subtitle,
|
||||
path,
|
||||
@@ -100,4 +100,4 @@ const SmallListItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallListItem;
|
||||
export default DoubleFieldListItem;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Menu, MenuItem } from "@material-ui/core";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io";
|
||||
import { MdMoreVert } from "react-icons/md";
|
||||
import ResumeContext from "../../../../contexts/ResumeContext";
|
||||
import styles from "./TripleFieldListItem.module.css";
|
||||
|
||||
const TripleFieldListItem = ({
|
||||
title,
|
||||
subtitle,
|
||||
text,
|
||||
path,
|
||||
data,
|
||||
isFirst,
|
||||
isLast,
|
||||
onEdit,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const { dispatch } = useContext(ResumeContext);
|
||||
|
||||
const handleClick = (event) => setAnchorEl(event.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveUp = () => {
|
||||
dispatch({
|
||||
type: "on_move_item_up",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
dispatch({
|
||||
type: "on_move_item_down",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch({
|
||||
type: "on_delete_item",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className="grid">
|
||||
<span className="font-medium">{title}</span>
|
||||
<span className="mt-1 text-sm opacity-75">{subtitle}</span>
|
||||
|
||||
<span className="w-4/5 mt-6 text-sm opacity-75 truncate">{text}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.menu}>
|
||||
<MdMoreVert
|
||||
size="18px"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
open={Boolean(anchorEl)}
|
||||
>
|
||||
<div className="flex items-center space-around">
|
||||
<MenuItem disabled={isFirst} onClick={handleMoveUp}>
|
||||
<IoIosArrowUp size="18px" />
|
||||
</MenuItem>
|
||||
<MenuItem disabled={isLast} onClick={handleMoveDown}>
|
||||
<IoIosArrowDown size="18px" />
|
||||
</MenuItem>
|
||||
</div>
|
||||
<MenuItem onClick={handleEdit}>Edit</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<span className="text-red-600 font-medium">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TripleFieldListItem;
|
||||
@@ -0,0 +1,17 @@
|
||||
.container {
|
||||
@apply flex items-center justify-between border-t border-secondary px-8 py-5;
|
||||
}
|
||||
|
||||
.container:first-child {
|
||||
@apply border-t-0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@apply opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.container:hover .menu {
|
||||
@apply opacity-100;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const RightSidebar = () => {
|
||||
<div className="flex">
|
||||
<div className={styles.container}>
|
||||
<Layout />
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<RightNavbar />
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
.container {
|
||||
z-index: 10;
|
||||
box-shadow: var(--right-shadow);
|
||||
@apply w-full h-screen p-8;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
@apply w-full h-screen overflow-scroll p-8;
|
||||
@apply grid gap-6;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { get } from "lodash";
|
||||
import React from "react";
|
||||
import Heading from "../../shared/Heading";
|
||||
import List from "../lists/List";
|
||||
import TripleFieldListItem from "../lists/triple/TripleFieldListItem";
|
||||
|
||||
const Education = ({ id, name, event, state }) => {
|
||||
const path = `${id}.items`;
|
||||
const items = get(state, path, []);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>{name}</Heading>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
items={items}
|
||||
event={event}
|
||||
titlePath="institution"
|
||||
textPath="field"
|
||||
listItemComponent={TripleFieldListItem}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Education;
|
||||
@@ -31,7 +31,11 @@ const Layout = () => {
|
||||
Block {ind + 1}
|
||||
</span>
|
||||
{el.map((item, index) => (
|
||||
<Draggable key={item} draggableId={item} index={index}>
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
@@ -39,7 +43,7 @@ const Layout = () => {
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{item}
|
||||
{item.name}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import Heading from "../../shared/Heading";
|
||||
import Input from "../../shared/Input";
|
||||
|
||||
const Objective = () => {
|
||||
return (
|
||||
<section>
|
||||
<Heading>Objective</Heading>
|
||||
|
||||
<Input type="textarea" label="Objective" path="objective" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Objective;
|
||||
@@ -1,38 +1,25 @@
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import ModalContext from "../../../contexts/ModalContext";
|
||||
import React from "react";
|
||||
import Heading from "../../shared/Heading";
|
||||
import DoubleFieldListItem from "../lists/double/DoubleFieldListItem";
|
||||
import List from "../lists/List";
|
||||
import SmallListItem from "../lists/small/SmallListItem";
|
||||
|
||||
const Social = ({ state }) => {
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
const path = "social.items";
|
||||
const Social = ({ id, name, event, state }) => {
|
||||
const path = `${id}.items`;
|
||||
const items = get(state, path, []);
|
||||
|
||||
const handleAdd = () => emitter.emit(events.SOCIAL_MODAL);
|
||||
|
||||
const handleEdit = (data) => emitter.emit(events.SOCIAL_MODAL, data);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>Social Network</Heading>
|
||||
<Heading>{name}</Heading>
|
||||
|
||||
<List items={items} onAdd={handleAdd}>
|
||||
{items.map((x, i) => (
|
||||
<SmallListItem
|
||||
key={x.id}
|
||||
data={x}
|
||||
path={path}
|
||||
title={x.network}
|
||||
isFirst={i === 0}
|
||||
subtitle={x.username}
|
||||
onEdit={() => handleEdit(x)}
|
||||
isLast={i === items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<List
|
||||
path={path}
|
||||
items={items}
|
||||
event={event}
|
||||
titlePath="network"
|
||||
subtitlePath="username"
|
||||
listItemComponent={DoubleFieldListItem}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { get } from "lodash";
|
||||
import React from "react";
|
||||
import Heading from "../../shared/Heading";
|
||||
import List from "../lists/List";
|
||||
import TripleFieldListItem from "../lists/triple/TripleFieldListItem";
|
||||
|
||||
const Work = ({ id, name, event, state }) => {
|
||||
const path = `${id}.items`;
|
||||
const items = get(state, path, []);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>{name}</Heading>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
items={items}
|
||||
event={event}
|
||||
titlePath="company"
|
||||
textPath="summary"
|
||||
listItemComponent={TripleFieldListItem}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Work;
|
||||
@@ -7,10 +7,6 @@
|
||||
@apply bg-primary-dark;
|
||||
}
|
||||
|
||||
.container:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.container.outline {
|
||||
@apply border border-primary bg-inverse text-primary;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import cx from "classnames";
|
||||
import { get } from "lodash";
|
||||
import { get, isFunction } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import ResumeContext from "../../contexts/ResumeContext";
|
||||
import { handleKeyDown } from "../../utils";
|
||||
import styles from "./Input.module.css";
|
||||
|
||||
const Input = ({
|
||||
@@ -17,6 +19,8 @@ const Input = ({
|
||||
onChange,
|
||||
className,
|
||||
placeholder,
|
||||
onDeleteItem,
|
||||
showDeleteItemButton,
|
||||
type = "text",
|
||||
}) => {
|
||||
const uuid = uuidv4();
|
||||
@@ -39,18 +43,59 @@ const Input = ({
|
||||
<div className={cx(styles.container, className)}>
|
||||
<label htmlFor={uuid}>
|
||||
<span>{label}</span>
|
||||
<input
|
||||
id={uuid}
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
{...(path && inputProps(path))}
|
||||
/>
|
||||
{touched && <p>{error}</p>}
|
||||
{type === "textarea" ? (
|
||||
<div className="flex flex-col">
|
||||
<textarea
|
||||
id={uuid}
|
||||
rows="4"
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
{...(path && inputProps(path))}
|
||||
/>
|
||||
|
||||
<p className="mt-2 text-sm opacity-75">
|
||||
This text block supports{" "}
|
||||
<a
|
||||
href="https://www.markdownguide.org/basic-syntax/"
|
||||
className="text-blue-600"
|
||||
target="blank"
|
||||
>
|
||||
markdown
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative grid items-center">
|
||||
<input
|
||||
id={uuid}
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
{...(path && inputProps(path))}
|
||||
/>
|
||||
|
||||
{showDeleteItemButton && isFunction(onDeleteItem) && (
|
||||
<MdClose
|
||||
size="16px"
|
||||
tabIndex="0"
|
||||
onClick={onDeleteItem}
|
||||
onKeyDown={(e) => handleKeyDown(e, onDeleteItem)}
|
||||
className="absolute right-0 cursor-pointer opacity-50 hover:opacity-75 mx-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{error && touched && <p>{error}</p>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,26 +2,21 @@
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.container > label {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.container > label > span {
|
||||
@apply mb-1 text-secondary-dark font-semibold tracking-wide text-xs uppercase;
|
||||
}
|
||||
|
||||
.container > label > input {
|
||||
.container label input,
|
||||
.container label textarea {
|
||||
@apply py-3 px-4 rounded bg-secondary text-primary border border-secondary;
|
||||
}
|
||||
|
||||
.container > label > input::placeholder {
|
||||
.container label input::placeholder,
|
||||
.container label textarea::placeholder {
|
||||
@apply text-primary opacity-50;
|
||||
}
|
||||
|
||||
.container > label > input:focus {
|
||||
.container label input:focus,
|
||||
.container label textarea:focus {
|
||||
@apply outline-none border-gray-500;
|
||||
}
|
||||
|
||||
.container > label > p {
|
||||
.container label > p {
|
||||
@apply mt-1 text-red-600 text-xs;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createNanoEvents } from "nanoevents";
|
||||
import React, { createContext } from "react";
|
||||
|
||||
const events = {
|
||||
const MODAL_EVENTS = {
|
||||
AUTH_MODAL: "auth_modal",
|
||||
CREATE_RESUME_MODAL: "create_resume_modal",
|
||||
SOCIAL_MODAL: "social_modal",
|
||||
WORK_MODAL: "work_modal",
|
||||
EDUCATION_MODAL: "education_modal",
|
||||
};
|
||||
|
||||
const emitter = createNanoEvents();
|
||||
|
||||
const defaultState = { events, emitter };
|
||||
const defaultState = { events: MODAL_EVENTS, emitter };
|
||||
|
||||
const ModalContext = createContext(defaultState);
|
||||
|
||||
@@ -23,4 +25,4 @@ const ModalProvider = ({ children }) => {
|
||||
|
||||
export default ModalContext;
|
||||
|
||||
export { ModalProvider };
|
||||
export { ModalProvider, MODAL_EVENTS };
|
||||
|
||||
@@ -21,6 +21,7 @@ const ResumeProvider = ({ children }) => {
|
||||
|
||||
switch (type) {
|
||||
case "on_add_item":
|
||||
delete payload.value.__temp;
|
||||
items = get(state, payload.path, []);
|
||||
newState = setWith(
|
||||
clone(state),
|
||||
@@ -32,6 +33,7 @@ const ResumeProvider = ({ children }) => {
|
||||
return newState;
|
||||
|
||||
case "on_edit_item":
|
||||
delete payload.value.__temp;
|
||||
items = get(state, payload.path);
|
||||
index = findIndex(items, ["id", payload.value.id]);
|
||||
newState = setWith(
|
||||
|
||||
@@ -10,7 +10,7 @@ const defaultState = {
|
||||
primaryColor: "#f44336",
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
blocks: [leftSections.map((x) => x.id)],
|
||||
blocks: [leftSections],
|
||||
setBlocks: () => {},
|
||||
setFixedBlocks: () => {},
|
||||
setSupportedBlocks: () => {},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { AiOutlineTwitter } from "react-icons/ai";
|
||||
import { MdPerson } from "react-icons/md";
|
||||
import { IoMdBriefcase, IoMdDocument } from "react-icons/io";
|
||||
import { MdPerson, MdSchool } from "react-icons/md";
|
||||
import Education from "../components/builder/sections/Education";
|
||||
import Objective from "../components/builder/sections/Objective";
|
||||
import Profile from "../components/builder/sections/Profile";
|
||||
import Social from "../components/builder/sections/Social";
|
||||
import Work from "../components/builder/sections/Work";
|
||||
import { MODAL_EVENTS } from "../contexts/ModalContext";
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -15,5 +20,26 @@ export default [
|
||||
name: "Social Network",
|
||||
icon: AiOutlineTwitter,
|
||||
component: Social,
|
||||
event: MODAL_EVENTS.SOCIAL_MODAL,
|
||||
},
|
||||
{
|
||||
id: "objective",
|
||||
name: "Objective",
|
||||
icon: IoMdDocument,
|
||||
component: Objective,
|
||||
},
|
||||
{
|
||||
id: "work",
|
||||
name: "Work Experience",
|
||||
icon: IoMdBriefcase,
|
||||
component: Work,
|
||||
event: MODAL_EVENTS.WORK_MODAL,
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
name: "Education",
|
||||
icon: MdSchool,
|
||||
component: Education,
|
||||
event: MODAL_EVENTS.EDUCATION_MODAL,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isFunction } from "lodash";
|
||||
import React, { forwardRef, useImperativeHandle } from "react";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import Button from "../components/shared/Button";
|
||||
import { handleKeyDown } from "../utils";
|
||||
import styles from "./BaseModal.module.css";
|
||||
|
||||
const BaseModal = forwardRef(
|
||||
@@ -33,7 +34,12 @@ const BaseModal = forwardRef(
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.title}>
|
||||
<h2>{title}</h2>
|
||||
<MdClose size="18" onClick={handleClose} />
|
||||
<MdClose
|
||||
size="18"
|
||||
tabIndex="0"
|
||||
onClick={handleClose}
|
||||
onKeyDown={(e) => handleKeyDown(e, handleClose)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>{children}</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useFormikContext } from "formik";
|
||||
import { isEmpty, isFunction } from "lodash";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -12,7 +13,6 @@ const DataModal = ({
|
||||
path,
|
||||
event,
|
||||
title,
|
||||
formik,
|
||||
onEdit,
|
||||
onCreate,
|
||||
children,
|
||||
@@ -25,6 +25,7 @@ const DataModal = ({
|
||||
|
||||
const { emitter } = useContext(ModalContext);
|
||||
const { dispatch } = useContext(ResumeContext);
|
||||
const { values, setValues, resetForm, validateForm } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
const unbind = emitter.on(event, (data) => {
|
||||
@@ -36,12 +37,12 @@ const DataModal = ({
|
||||
}, [emitter, event]);
|
||||
|
||||
useEffect(() => {
|
||||
data && formik.setValues(data) && setEditMode(true);
|
||||
data && setValues(data) && setEditMode(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (newData) => {
|
||||
if (isEmpty(await formik.validateForm(newData))) {
|
||||
if (isEmpty(await validateForm(newData))) {
|
||||
if (isEditMode) {
|
||||
if (data !== newData) {
|
||||
isFunction(onEdit)
|
||||
@@ -79,15 +80,11 @@ const DataModal = ({
|
||||
: title.create;
|
||||
|
||||
const submitAction = (
|
||||
<Button
|
||||
type="submit"
|
||||
title={getTitle}
|
||||
onClick={() => onSubmit(formik.values)}
|
||||
/>
|
||||
<Button type="submit" title={getTitle} onClick={() => onSubmit(values)} />
|
||||
);
|
||||
|
||||
const onDestroy = () => {
|
||||
formik.resetForm();
|
||||
resetForm();
|
||||
setEditMode(false);
|
||||
setData(null);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { Fragment } from "react";
|
||||
import AuthModal from "./AuthModal";
|
||||
import ResumeModal from "./ResumeModal";
|
||||
import EducationModal from "./sections/EducationModal";
|
||||
import SocialModal from "./sections/SocialModal";
|
||||
import WorkModal from "./sections/WorkModal";
|
||||
|
||||
const ModalRegistrar = () => {
|
||||
return (
|
||||
@@ -9,6 +11,8 @@ const ModalRegistrar = () => {
|
||||
<AuthModal />
|
||||
<ResumeModal />
|
||||
<SocialModal />
|
||||
<WorkModal />
|
||||
<EducationModal />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
+35
-33
@@ -1,4 +1,5 @@
|
||||
import { useFormik } from "formik";
|
||||
import { Formik } from "formik";
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import * as Yup from "yup";
|
||||
import Input from "../components/shared/Input";
|
||||
@@ -20,44 +21,45 @@ const ResumeModal = () => {
|
||||
const { events } = useContext(ModalContext);
|
||||
const { createResume, updateResume } = useContext(DatabaseContext);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
});
|
||||
|
||||
const getFieldProps = (name) => ({
|
||||
const getFieldProps = (formik, name) => ({
|
||||
touched: get(formik, `touched.${name}`, false),
|
||||
error: get(formik, `errors.${name}`, ""),
|
||||
...formik.getFieldProps(name),
|
||||
error: formik.errors[name],
|
||||
});
|
||||
|
||||
return (
|
||||
<DataModal
|
||||
name="Resume"
|
||||
formik={formik}
|
||||
title={{
|
||||
create: "Create Resume",
|
||||
edit: "Edit Resume",
|
||||
}}
|
||||
onEdit={updateResume}
|
||||
onCreate={createResume}
|
||||
event={events.CREATE_RESUME_MODAL}
|
||||
<Formik
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
label="Name"
|
||||
className="mb-8"
|
||||
placeholder="Full Stack Web Developer"
|
||||
{...getFieldProps("name")}
|
||||
/>
|
||||
{(formik) => (
|
||||
<DataModal
|
||||
name="Resume"
|
||||
title={{
|
||||
create: "Create Resume",
|
||||
edit: "Edit Resume",
|
||||
}}
|
||||
onEdit={updateResume}
|
||||
onCreate={createResume}
|
||||
event={events.CREATE_RESUME_MODAL}
|
||||
>
|
||||
<Input
|
||||
label="Name"
|
||||
className="mb-8"
|
||||
placeholder="Full Stack Web Developer"
|
||||
{...getFieldProps(formik, "name")}
|
||||
/>
|
||||
|
||||
<p>
|
||||
You are going to be creating a new resume from scratch, but first, let's
|
||||
give it a name. This can be the name of the role you want to apply for,
|
||||
or if you're making a resume for a friend, you could call it Alex's
|
||||
Resume.
|
||||
</p>
|
||||
</DataModal>
|
||||
<p>
|
||||
You are going to be creating a new resume from scratch, but first,
|
||||
let's give it a name. This can be the name of the role you want to
|
||||
apply for, or if you're making a resume for a friend, you could call
|
||||
it Alex's Resume.
|
||||
</p>
|
||||
</DataModal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Field, FieldArray, Formik } from "formik";
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import * as Yup from "yup";
|
||||
import Input from "../../components/shared/Input";
|
||||
import ModalContext from "../../contexts/ModalContext";
|
||||
import { handleKeyDown } from "../../utils";
|
||||
import DataModal from "../DataModal";
|
||||
|
||||
const initialValues = {
|
||||
institution: "",
|
||||
field: "",
|
||||
degree: "",
|
||||
gpa: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
courses: [],
|
||||
__temp: "",
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
institution: Yup.string().required("This is a required field."),
|
||||
field: Yup.string().required("This is a required field."),
|
||||
degree: Yup.string(),
|
||||
gpa: Yup.string(),
|
||||
startDate: Yup.date().required("This is a required field."),
|
||||
endDate: Yup.date().when(
|
||||
"startDate",
|
||||
(startDate, schema) =>
|
||||
startDate &&
|
||||
schema.min(startDate, "End Date must be later than Start Date")
|
||||
),
|
||||
courses: Yup.array().of(Yup.string().required("This is a required field.")),
|
||||
__temp: Yup.string().ensure(),
|
||||
});
|
||||
|
||||
const EducationModal = () => {
|
||||
const { events } = useContext(ModalContext);
|
||||
|
||||
const getFieldProps = (formik, name) => ({
|
||||
touched: get(formik, `touched.${name}`, false),
|
||||
error: get(formik, `errors.${name}`, ""),
|
||||
...formik.getFieldProps(name),
|
||||
});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) => (
|
||||
<DataModal
|
||||
path="education.items"
|
||||
name="Education"
|
||||
event={events.EDUCATION_MODAL}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<Input
|
||||
label="Institution"
|
||||
className="col-span-2"
|
||||
placeholder="Dayananda Sagar College of Engineering"
|
||||
{...getFieldProps(formik, "institution")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Field of Study"
|
||||
className="col-span-2"
|
||||
placeholder="Computer Science & Engineering"
|
||||
{...getFieldProps(formik, "field")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Degree Type"
|
||||
placeholder="Bachelor's Degree"
|
||||
{...getFieldProps(formik, "degree")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="GPA"
|
||||
placeholder="8.8"
|
||||
{...getFieldProps(formik, "gpa")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
label="Start Date"
|
||||
placeholder="6th August 208"
|
||||
{...getFieldProps(formik, "startDate")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
label="End Date"
|
||||
placeholder="6th August 208"
|
||||
{...getFieldProps(formik, "endDate")}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
name="courses"
|
||||
render={(arrayHelpers) => {
|
||||
const handleClickAdd = () => {
|
||||
formik.values.__temp &&
|
||||
arrayHelpers.push(formik.values.__temp);
|
||||
formik.setFieldValue("__temp", "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-span-2">
|
||||
<label>
|
||||
<span>Courses</span>
|
||||
|
||||
{formik.values.courses &&
|
||||
formik.values.courses.map((x, i) => (
|
||||
<Field key={i} name={`courses.${i}`}>
|
||||
{({ field, meta }) => (
|
||||
<Input
|
||||
className="my-1"
|
||||
showDeleteItemButton
|
||||
onDeleteItem={() => arrayHelpers.remove(i)}
|
||||
{...field}
|
||||
{...meta}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
placeholder="Algorithms & Data Structures"
|
||||
{...getFieldProps(formik, "__temp")}
|
||||
/>
|
||||
<MdAdd
|
||||
size="18px"
|
||||
tabIndex="0"
|
||||
className="mx-4 cursor-pointer opacity-50 hover:opacity-75"
|
||||
onKeyDown={(e) => handleKeyDown(e, handleClickAdd)}
|
||||
onClick={handleClickAdd}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DataModal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationModal;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFormik } from "formik";
|
||||
import { Formik } from "formik";
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import * as Yup from "yup";
|
||||
import Input from "../../components/shared/Input";
|
||||
@@ -25,52 +26,47 @@ const validationSchema = Yup.object().shape({
|
||||
const SocialModal = () => {
|
||||
const { events } = useContext(ModalContext);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
validateOnBlur: true,
|
||||
});
|
||||
|
||||
const getFieldProps = (name) => ({
|
||||
const getFieldProps = (formik, name) => ({
|
||||
touched: get(formik, `touched.${name}`, false),
|
||||
error: get(formik, `errors.${name}`, ""),
|
||||
...formik.getFieldProps(name),
|
||||
touched: formik.touched[name],
|
||||
error: formik.errors[name],
|
||||
});
|
||||
|
||||
return (
|
||||
<DataModal
|
||||
formik={formik}
|
||||
path="social.items"
|
||||
name="Social Network"
|
||||
event={events.SOCIAL_MODAL}
|
||||
<Formik
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<Input
|
||||
type="text"
|
||||
name="network"
|
||||
label="Network"
|
||||
placeholder="Twitter"
|
||||
{...getFieldProps("network")}
|
||||
/>
|
||||
{(formik) => (
|
||||
<DataModal
|
||||
path="social.items"
|
||||
name="Social Network"
|
||||
event={events.SOCIAL_MODAL}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<Input
|
||||
label="Network"
|
||||
placeholder="Twitter"
|
||||
{...getFieldProps(formik, "network")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
name="username"
|
||||
label="Username"
|
||||
placeholder="KingOKings"
|
||||
{...getFieldProps("username")}
|
||||
/>
|
||||
<Input
|
||||
label="Username"
|
||||
placeholder="KingOKings"
|
||||
{...getFieldProps(formik, "username")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
name="url"
|
||||
label="URL"
|
||||
className="col-span-2"
|
||||
placeholder="https://twitter.com/KingOKings"
|
||||
{...getFieldProps("url")}
|
||||
/>
|
||||
</div>
|
||||
</DataModal>
|
||||
<Input
|
||||
label="URL"
|
||||
className="col-span-2"
|
||||
placeholder="https://twitter.com/KingOKings"
|
||||
{...getFieldProps(formik, "url")}
|
||||
/>
|
||||
</div>
|
||||
</DataModal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Field, FieldArray, Formik } from "formik";
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import * as Yup from "yup";
|
||||
import Input from "../../components/shared/Input";
|
||||
import ModalContext from "../../contexts/ModalContext";
|
||||
import { handleKeyDown } from "../../utils";
|
||||
import DataModal from "../DataModal";
|
||||
|
||||
const initialValues = {
|
||||
company: "",
|
||||
position: "",
|
||||
website: "https://",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
summary: "",
|
||||
highlights: [],
|
||||
__temp: "",
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
company: Yup.string().required("This is a required field."),
|
||||
position: Yup.string().required("This is a required field."),
|
||||
website: Yup.string().url("Must be a valid URL"),
|
||||
startDate: Yup.date().required("This is a required field."),
|
||||
endDate: Yup.date().when(
|
||||
"startDate",
|
||||
(startDate, schema) =>
|
||||
startDate &&
|
||||
schema.min(startDate, "End Date must be later than Start Date")
|
||||
),
|
||||
summary: Yup.string().min(10, "Please enter at least 10 characters."),
|
||||
highlights: Yup.array().of(
|
||||
Yup.string().required("This is a required field.")
|
||||
),
|
||||
__temp: Yup.string().ensure(),
|
||||
});
|
||||
|
||||
const WorkModal = () => {
|
||||
const { events } = useContext(ModalContext);
|
||||
|
||||
const getFieldProps = (formik, name) => ({
|
||||
touched: get(formik, `touched.${name}`, false),
|
||||
error: get(formik, `errors.${name}`, ""),
|
||||
...formik.getFieldProps(name),
|
||||
});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validateOnBlur
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) => (
|
||||
<DataModal
|
||||
path="work.items"
|
||||
name="Work Experience"
|
||||
event={events.WORK_MODAL}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<Input
|
||||
label="Company"
|
||||
className="col-span-2"
|
||||
placeholder="Postdot Technologies Pvt. Ltd."
|
||||
{...getFieldProps(formik, "company")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Position"
|
||||
placeholder="Full Stack Web Developer"
|
||||
{...getFieldProps(formik, "position")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Website"
|
||||
placeholder="https://example.com/"
|
||||
{...getFieldProps(formik, "website")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
label="Start Date"
|
||||
placeholder="6th August 208"
|
||||
{...getFieldProps(formik, "startDate")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
label="End Date"
|
||||
placeholder="6th August 208"
|
||||
{...getFieldProps(formik, "endDate")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="textarea"
|
||||
label="Summary"
|
||||
className="col-span-2"
|
||||
{...getFieldProps(formik, "summary")}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
name="highlights"
|
||||
render={(arrayHelpers) => {
|
||||
const handleClickAdd = () => {
|
||||
formik.values.__temp &&
|
||||
arrayHelpers.push(formik.values.__temp);
|
||||
formik.setFieldValue("__temp", "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-span-2">
|
||||
<label>
|
||||
<span>Highlights</span>
|
||||
|
||||
{formik.values.highlights &&
|
||||
formik.values.highlights.map((x, i) => (
|
||||
<Field key={i} name={`highlights.${i}`}>
|
||||
{({ field, meta }) => (
|
||||
<Input
|
||||
className="my-1"
|
||||
showDeleteItemButton
|
||||
onDeleteItem={() => arrayHelpers.remove(i)}
|
||||
{...field}
|
||||
{...meta}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
placeholder="Worked passionately in customer service in a high volume restaurant."
|
||||
{...getFieldProps(formik, "__temp")}
|
||||
/>
|
||||
<MdAdd
|
||||
size="18px"
|
||||
tabIndex="0"
|
||||
className="mx-4 cursor-pointer opacity-50 hover:opacity-75"
|
||||
onKeyDown={(e) => handleKeyDown(e, handleClickAdd)}
|
||||
onClick={handleClickAdd}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DataModal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkModal;
|
||||
+12
-6
@@ -33,12 +33,18 @@ const Home = () => {
|
||||
|
||||
<Feature title="Kickstarting your career shouldn’t come at a cost.">
|
||||
There are brilliant alternatives to this app like{" "}
|
||||
<a href="/">Novoresume</a> and <a href="/">Zety</a>, but they come
|
||||
at a cost, mainly because of the time the developers and the
|
||||
marketing they had to incur to make the product. This app might not
|
||||
be better than them, but it does cater to people who are just not in
|
||||
a position to pay hundreds of dollars to create a resume to
|
||||
bootstrap their career.
|
||||
<a href="/" target="blank">
|
||||
Novoresume
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="/" target="blank">
|
||||
Zety
|
||||
</a>
|
||||
, but they come at a cost, mainly because of the time the developers
|
||||
and the marketing they had to incur to make the product. This app
|
||||
might not be better than them, but it does cater to people who are
|
||||
just not in a position to pay hundreds of dollars to create a resume
|
||||
to bootstrap their career.
|
||||
</Feature>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,6 +29,14 @@ section {
|
||||
@apply grid grid-cols-1 gap-8;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
label > span:first-child {
|
||||
@apply mb-1 text-secondary-dark font-semibold tracking-wide text-xs uppercase;
|
||||
}
|
||||
|
||||
#artboard {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export const transformCollectionSnapshot = (snapshot, setData) => {
|
||||
};
|
||||
|
||||
export const handleKeyDown = (event, action) => {
|
||||
event.which === 13 && action();
|
||||
(event.which === 13 || event.which === 32) && action();
|
||||
};
|
||||
|
||||
export const isFileImage = (file) => {
|
||||
|
||||
Reference in New Issue
Block a user