- implement lists

- implement generic sections
- implement list actions
- implement error handlers
This commit is contained in:
Amruth Pillai
2020-07-08 05:01:50 +05:30
parent d7e86ddf29
commit bee6a40e9f
38 changed files with 762 additions and 177 deletions

View File

@ -1,4 +1,5 @@
import React, { useContext } from "react";
import { Helmet } from "react-helmet";
import ResumeContext from "../../../contexts/ResumeContext";
import TemplateContext from "../../../contexts/TemplateContext";
import Onyx from "../../../templates/Onyx";
@ -9,8 +10,18 @@ const Artboard = () => {
const { state } = useContext(ResumeContext);
return (
<div id="artboard" className={styles.container}>
<Onyx data={state} layout={blocks} colors={colors} />
<div>
<Helmet>
<title>{state.name} | Reactive Resume</title>
<link
rel="canonical"
href={`https://rxresu.me/app/builder/${state.id}`}
/>
</Helmet>
<div id="artboard" className={styles.container}>
<Onyx data={state} layout={blocks} colors={colors} />
</div>
</div>
);
};

View File

@ -1,9 +1,12 @@
import React, { Fragment } from "react";
import React, { Fragment, useContext } from "react";
import ResumeContext from "../../../contexts/ResumeContext";
import sections from "../../../data/leftSections";
import LeftNavbar from "./LeftNavbar";
import styles from "./LeftSidebar.module.css";
const LeftSidebar = () => {
const { state } = useContext(ResumeContext);
return (
<div className="flex">
<LeftNavbar />
@ -11,7 +14,7 @@ const LeftSidebar = () => {
<div className={styles.container}>
{sections.map(({ id, component: Component }) => (
<Fragment key={id}>
<Component />
<Component state={state} />
<hr />
</Fragment>
))}

View File

@ -0,0 +1,9 @@
import React from "react";
const EmptyList = () => (
<div className="rounded border border-secondary py-6 text-center">
This list is empty.
</div>
);
export default EmptyList;

View File

@ -0,0 +1,26 @@
import { isEmpty } from "lodash";
import React from "react";
import { MdAdd } from "react-icons/md";
import Button from "../../shared/Button";
import EmptyList from "./EmptyList";
import styles from "./List.module.css";
const List = ({ items, onAdd, children }) => {
return (
<div className="flex flex-col">
<div className={styles.container}>
{isEmpty(items) ? <EmptyList /> : children}
</div>
<Button
outline
icon={MdAdd}
title="Add New"
onClick={onAdd}
className="mt-8 ml-auto"
/>
</div>
);
};
export default List;

View File

@ -0,0 +1,3 @@
.container {
@apply flex flex-col border border-secondary rounded;
}

View File

@ -0,0 +1,103 @@
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 "./SmallListItem.module.css";
const SmallListItem = ({
title,
subtitle,
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="flex flex-col">
<span className="font-medium">{title}</span>
<span className="mt-1 text-sm opacity-75">{subtitle}</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 SmallListItem;

View File

@ -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;
}

View File

@ -1,19 +1,45 @@
import React from "react";
import React, { useContext, useRef } from "react";
import { MdFileUpload } from "react-icons/md";
import StorageContext from "../../../contexts/StorageContext";
import { handleKeyDown } from "../../../utils";
import Heading from "../../shared/Heading";
import Input from "../../shared/Input";
import styles from "./Profile.module.css";
const Profile = () => {
const fileInputRef = useRef(null);
const { uploadPhotograph } = useContext(StorageContext);
const handleIconClick = () => {
fileInputRef.current.click();
};
const handleImageUpload = (e) => {
const file = e.target.files[0];
uploadPhotograph(file);
};
return (
<section>
<Heading>Profile</Heading>
<div className="flex items-center">
<div className={styles.circle}>
<div
role="button"
tabIndex="0"
className={styles.circle}
onClick={handleIconClick}
onKeyDown={(e) => handleKeyDown(e, handleIconClick)}
>
<MdFileUpload size="22px" />
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleImageUpload}
/>
</div>
<Input label="Photograph" className="ml-6" path="profile.photograph" />
<Input label="Photograph" className="pl-6" path="profile.photograph" />
</div>
<div className="grid grid-cols-2 gap-6">

View File

@ -2,5 +2,5 @@
width: 60px;
height: 60px;
flex: 0 0 60px;
@apply flex items-center justify-center bg-secondary text-secondary-dark rounded-full;
@apply flex items-center justify-center cursor-pointer bg-secondary text-secondary-dark rounded-full;
}

View File

@ -0,0 +1,40 @@
import { get } from "lodash";
import React, { useContext } from "react";
import ModalContext from "../../../contexts/ModalContext";
import Heading from "../../shared/Heading";
import List from "../lists/List";
import SmallListItem from "../lists/small/SmallListItem";
const Social = ({ state }) => {
const { emitter, events } = useContext(ModalContext);
const path = "social.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>
<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>
</section>
);
};
export default Social;

View File

@ -1,12 +0,0 @@
import React from "react";
import Heading from "../../shared/Heading";
const SocialNetwork = () => {
return (
<section>
<Heading>Social Network</Heading>
</section>
);
};
export default SocialNetwork;

View File

@ -1,6 +1,7 @@
import React, { useContext } from "react";
import { MdAdd } from "react-icons/md";
import ModalContext from "../../contexts/ModalContext";
import { handleKeyDown } from "../../utils";
import styles from "./CreateResume.module.css";
const CreateResume = () => {
@ -18,12 +19,12 @@ const CreateResume = () => {
role="button"
className={styles.page}
onClick={handleClick}
onKeyDown={() => {}}
onKeyDown={(e) => handleKeyDown(e, handleClick)}
>
<MdAdd size="48" />
</div>
<div className={styles.meta}>
<p>Create New Resume</p>
<p>Create Resume</p>
</div>
</div>
);

View File

@ -64,7 +64,7 @@ const ResumePreview = ({ resume }) => {
>
<MenuItem onClick={handleRename}>Rename</MenuItem>
<MenuItem onClick={handleDelete}>
<span className="text-red-600">Delete</span>
<span className="text-red-600 font-medium">Delete</span>
</MenuItem>
</Menu>
</div>

View File

@ -7,7 +7,7 @@ const PrivateRoute = ({ component: Component, location, ...props }) => {
const { user, loading } = useContext(UserContext);
if (loading) {
return <LoadingScreen message="Authenticating..." />;
return <LoadingScreen />;
}
if (!user) {

View File

@ -1,15 +1,18 @@
import cx from "classnames";
import React, { useContext } from "react";
import { toUrl } from "gatsby-source-gravatar";
import React, { useContext, useMemo } from "react";
import UserContext from "../../contexts/UserContext";
import styles from "./Avatar.module.css";
const Avatar = ({ className }) => {
const { user } = useContext(UserContext);
const photoURL = useMemo(() => toUrl(user.email, "size=128"), [user.email]);
return (
<img
className={cx(styles.container, className)}
src={user.photoURL}
src={photoURL}
alt={user.displayName}
/>
);

View File

@ -1,5 +1,6 @@
import classNames from "classnames";
import React from "react";
import { handleKeyDown } from "../../utils";
import styles from "./Button.module.css";
const Button = ({
@ -16,13 +17,11 @@ const Button = ({
[styles.outline]: outline,
});
const handleKeyDown = () => {};
return (
<button
type={type}
className={classes}
onKeyDown={handleKeyDown}
onKeyDown={(e) => handleKeyDown(e, onClick)}
onClick={isLoading ? undefined : onClick}
>
{icon && <Icon size="14" className="mr-2" />}

View File

@ -11,6 +11,9 @@ const Input = ({
label,
value,
error,
onBlur,
touched,
checked,
onChange,
className,
placeholder,
@ -41,11 +44,13 @@ const Input = ({
name={name}
type={type}
value={value}
onBlur={onBlur}
checked={checked}
onChange={onChange}
placeholder={placeholder}
{...(path && inputProps(path))}
/>
<p>{error}</p>
{touched && <p>{error}</p>}
</label>
</div>
);