mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-16 17:51:43 +10:00
- implement lists
- implement generic sections - implement list actions - implement error handlers
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
9
src/components/builder/lists/EmptyList.js
Normal file
9
src/components/builder/lists/EmptyList.js
Normal 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;
|
||||
26
src/components/builder/lists/List.js
Normal file
26
src/components/builder/lists/List.js
Normal 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;
|
||||
3
src/components/builder/lists/List.module.css
Normal file
3
src/components/builder/lists/List.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.container {
|
||||
@apply flex flex-col border border-secondary rounded;
|
||||
}
|
||||
103
src/components/builder/lists/small/SmallListItem.js
Normal file
103
src/components/builder/lists/small/SmallListItem.js
Normal 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;
|
||||
17
src/components/builder/lists/small/SmallListItem.module.css
Normal file
17
src/components/builder/lists/small/SmallListItem.module.css
Normal 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;
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
40
src/components/builder/sections/Social.js
Normal file
40
src/components/builder/sections/Social.js
Normal 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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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" />}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user