mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 08:13:49 +10:00
- implement lists
- implement generic sections - implement list actions - implement error handlers
This commit is contained in:
@ -2,10 +2,12 @@ import { createMuiTheme, MuiThemeProvider } from "@material-ui/core";
|
|||||||
import "firebase/analytics";
|
import "firebase/analytics";
|
||||||
import "firebase/auth";
|
import "firebase/auth";
|
||||||
import "firebase/database";
|
import "firebase/database";
|
||||||
|
import "firebase/storage";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DatabaseProvider } from "./src/contexts/DatabaseContext";
|
import { DatabaseProvider } from "./src/contexts/DatabaseContext";
|
||||||
import { ModalProvider } from "./src/contexts/ModalContext";
|
import { ModalProvider } from "./src/contexts/ModalContext";
|
||||||
import { ResumeProvider } from "./src/contexts/ResumeContext";
|
import { ResumeProvider } from "./src/contexts/ResumeContext";
|
||||||
|
import { StorageProvider } from "./src/contexts/StorageContext";
|
||||||
import { TemplateProvider } from "./src/contexts/TemplateContext";
|
import { TemplateProvider } from "./src/contexts/TemplateContext";
|
||||||
import { ThemeProvider } from "./src/contexts/ThemeContext";
|
import { ThemeProvider } from "./src/contexts/ThemeContext";
|
||||||
import { UserProvider } from "./src/contexts/UserContext";
|
import { UserProvider } from "./src/contexts/UserContext";
|
||||||
@ -29,7 +31,9 @@ export const wrapRootElement = ({ element }) => (
|
|||||||
<UserProvider>
|
<UserProvider>
|
||||||
<DatabaseProvider>
|
<DatabaseProvider>
|
||||||
<ResumeProvider>
|
<ResumeProvider>
|
||||||
<TemplateProvider>{element}</TemplateProvider>
|
<StorageProvider>
|
||||||
|
<TemplateProvider>{element}</TemplateProvider>
|
||||||
|
</StorageProvider>
|
||||||
</ResumeProvider>
|
</ResumeProvider>
|
||||||
</DatabaseProvider>
|
</DatabaseProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ require("dotenv").config();
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
`gatsby-plugin-react-helmet`,
|
||||||
{
|
{
|
||||||
resolve: `gatsby-plugin-prefetch-google-fonts`,
|
resolve: `gatsby-plugin-prefetch-google-fonts`,
|
||||||
options: {
|
options: {
|
||||||
@ -24,7 +25,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
`gatsby-plugin-lodash`,
|
`gatsby-plugin-lodash`,
|
||||||
`gatsby-plugin-react-helmet`,
|
|
||||||
{
|
{
|
||||||
resolve: `gatsby-plugin-manifest`,
|
resolve: `gatsby-plugin-manifest`,
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import "firebase/analytics";
|
import "firebase/analytics";
|
||||||
import "firebase/auth";
|
import "firebase/auth";
|
||||||
import "firebase/database";
|
import "firebase/database";
|
||||||
|
import "firebase/storage";
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -3552,6 +3552,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
||||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI="
|
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI="
|
||||||
},
|
},
|
||||||
|
"array-move": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-lKc6C+nsOSA1o7eHSP/HshlGDYUI7QKyaus5kPDm2zEEPQID9xlspnraLR8l+rDlqg9mGo8ziE7F8TEnF6D3Tw=="
|
||||||
|
},
|
||||||
"array-reduce": {
|
"array-reduce": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||||
@ -9469,6 +9474,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"gatsby-source-gravatar": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gatsby-source-gravatar/-/gatsby-source-gravatar-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pN48dBm1oinhhrtypdOOgRu0nO3ZPARdZYKdTwIm8Pyfql1BbHUHwLfYf8E14SFkthHkUnXLh63q/qiQIGvqbw=="
|
||||||
|
},
|
||||||
"gatsby-telemetry": {
|
"gatsby-telemetry": {
|
||||||
"version": "1.3.18",
|
"version": "1.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-1.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-1.3.18.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
|
"array-move": "^2.2.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"firebase": "^7.15.5",
|
"firebase": "^7.15.5",
|
||||||
"formik": "^2.1.4",
|
"formik": "^2.1.4",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"gatsby-plugin-react-helmet": "^3.3.9",
|
"gatsby-plugin-react-helmet": "^3.3.9",
|
||||||
"gatsby-plugin-sharp": "^2.6.18",
|
"gatsby-plugin-sharp": "^2.6.18",
|
||||||
"gatsby-source-filesystem": "^2.3.18",
|
"gatsby-source-filesystem": "^2.3.18",
|
||||||
|
"gatsby-source-gravatar": "^1.0.0",
|
||||||
"gatsby-transformer-sharp": "^2.5.10",
|
"gatsby-transformer-sharp": "^2.5.10",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.27.0",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
import ResumeContext from "../../../contexts/ResumeContext";
|
import ResumeContext from "../../../contexts/ResumeContext";
|
||||||
import TemplateContext from "../../../contexts/TemplateContext";
|
import TemplateContext from "../../../contexts/TemplateContext";
|
||||||
import Onyx from "../../../templates/Onyx";
|
import Onyx from "../../../templates/Onyx";
|
||||||
@ -9,8 +10,18 @@ const Artboard = () => {
|
|||||||
const { state } = useContext(ResumeContext);
|
const { state } = useContext(ResumeContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="artboard" className={styles.container}>
|
<div>
|
||||||
<Onyx data={state} layout={blocks} colors={colors} />
|
<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>
|
</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 sections from "../../../data/leftSections";
|
||||||
import LeftNavbar from "./LeftNavbar";
|
import LeftNavbar from "./LeftNavbar";
|
||||||
import styles from "./LeftSidebar.module.css";
|
import styles from "./LeftSidebar.module.css";
|
||||||
|
|
||||||
const LeftSidebar = () => {
|
const LeftSidebar = () => {
|
||||||
|
const { state } = useContext(ResumeContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LeftNavbar />
|
<LeftNavbar />
|
||||||
@ -11,7 +14,7 @@ const LeftSidebar = () => {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{sections.map(({ id, component: Component }) => (
|
{sections.map(({ id, component: Component }) => (
|
||||||
<Fragment key={id}>
|
<Fragment key={id}>
|
||||||
<Component />
|
<Component state={state} />
|
||||||
<hr />
|
<hr />
|
||||||
</Fragment>
|
</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 { MdFileUpload } from "react-icons/md";
|
||||||
|
import StorageContext from "../../../contexts/StorageContext";
|
||||||
|
import { handleKeyDown } from "../../../utils";
|
||||||
import Heading from "../../shared/Heading";
|
import Heading from "../../shared/Heading";
|
||||||
import Input from "../../shared/Input";
|
import Input from "../../shared/Input";
|
||||||
import styles from "./Profile.module.css";
|
import styles from "./Profile.module.css";
|
||||||
|
|
||||||
const Profile = () => {
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<Heading>Profile</Heading>
|
<Heading>Profile</Heading>
|
||||||
<div className="flex items-center">
|
<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" />
|
<MdFileUpload size="22px" />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImageUpload}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input label="Photograph" className="ml-6" path="profile.photograph" />
|
<Input label="Photograph" className="pl-6" path="profile.photograph" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
flex: 0 0 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 React, { useContext } from "react";
|
||||||
import { MdAdd } from "react-icons/md";
|
import { MdAdd } from "react-icons/md";
|
||||||
import ModalContext from "../../contexts/ModalContext";
|
import ModalContext from "../../contexts/ModalContext";
|
||||||
|
import { handleKeyDown } from "../../utils";
|
||||||
import styles from "./CreateResume.module.css";
|
import styles from "./CreateResume.module.css";
|
||||||
|
|
||||||
const CreateResume = () => {
|
const CreateResume = () => {
|
||||||
@ -18,12 +19,12 @@ const CreateResume = () => {
|
|||||||
role="button"
|
role="button"
|
||||||
className={styles.page}
|
className={styles.page}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onKeyDown={() => {}}
|
onKeyDown={(e) => handleKeyDown(e, handleClick)}
|
||||||
>
|
>
|
||||||
<MdAdd size="48" />
|
<MdAdd size="48" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<p>Create New Resume</p>
|
<p>Create Resume</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -64,7 +64,7 @@ const ResumePreview = ({ resume }) => {
|
|||||||
>
|
>
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
<MenuItem onClick={handleDelete}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<span className="text-red-600">Delete</span>
|
<span className="text-red-600 font-medium">Delete</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const PrivateRoute = ({ component: Component, location, ...props }) => {
|
|||||||
const { user, loading } = useContext(UserContext);
|
const { user, loading } = useContext(UserContext);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingScreen message="Authenticating..." />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import cx from "classnames";
|
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 UserContext from "../../contexts/UserContext";
|
||||||
import styles from "./Avatar.module.css";
|
import styles from "./Avatar.module.css";
|
||||||
|
|
||||||
const Avatar = ({ className }) => {
|
const Avatar = ({ className }) => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const photoURL = useMemo(() => toUrl(user.email, "size=128"), [user.email]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={cx(styles.container, className)}
|
className={cx(styles.container, className)}
|
||||||
src={user.photoURL}
|
src={photoURL}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { handleKeyDown } from "../../utils";
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
@ -16,13 +17,11 @@ const Button = ({
|
|||||||
[styles.outline]: outline,
|
[styles.outline]: outline,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleKeyDown = () => {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={classes}
|
className={classes}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={(e) => handleKeyDown(e, onClick)}
|
||||||
onClick={isLoading ? undefined : onClick}
|
onClick={isLoading ? undefined : onClick}
|
||||||
>
|
>
|
||||||
{icon && <Icon size="14" className="mr-2" />}
|
{icon && <Icon size="14" className="mr-2" />}
|
||||||
|
|||||||
@ -11,6 +11,9 @@ const Input = ({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
error,
|
error,
|
||||||
|
onBlur,
|
||||||
|
touched,
|
||||||
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
placeholder,
|
placeholder,
|
||||||
@ -41,11 +44,13 @@ const Input = ({
|
|||||||
name={name}
|
name={name}
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...(path && inputProps(path))}
|
{...(path && inputProps(path))}
|
||||||
/>
|
/>
|
||||||
<p>{error}</p>
|
{touched && <p>{error}</p>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import firebase from "gatsby-plugin-firebase";
|
import firebase from "gatsby-plugin-firebase";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import UserContext from "./UserContext";
|
import UserContext from "./UserContext";
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
@ -38,27 +37,22 @@ const DatabaseProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createResume = (resume) => {
|
const createResume = (resume) => {
|
||||||
const id = uuidv4();
|
const { id } = resume;
|
||||||
const createdAt = firebase.database.ServerValue.TIMESTAMP;
|
const createdAt = firebase.database.ServerValue.TIMESTAMP;
|
||||||
|
|
||||||
let firstName = "",
|
let firstName, lastName;
|
||||||
lastName = "",
|
|
||||||
photograph = "";
|
|
||||||
|
|
||||||
if (!user.isAnonymous) {
|
if (!user.isAnonymous) {
|
||||||
[firstName, lastName] = user.displayName.split(" ");
|
[firstName, lastName] = user.displayName.split(" ");
|
||||||
photograph = user.photoURL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firebase
|
firebase
|
||||||
.database()
|
.database()
|
||||||
.ref(`users/${user.uid}/resumes/${id}`)
|
.ref(`users/${user.uid}/resumes/${id}`)
|
||||||
.set({
|
.set({
|
||||||
id,
|
|
||||||
profile: {
|
profile: {
|
||||||
firstName,
|
firstName: firstName || "",
|
||||||
lastName,
|
lastName: lastName || "",
|
||||||
photograph,
|
|
||||||
},
|
},
|
||||||
...resume,
|
...resume,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import React, { createContext } from "react";
|
|||||||
const events = {
|
const events = {
|
||||||
AUTH_MODAL: "auth_modal",
|
AUTH_MODAL: "auth_modal",
|
||||||
CREATE_RESUME_MODAL: "create_resume_modal",
|
CREATE_RESUME_MODAL: "create_resume_modal",
|
||||||
|
SOCIAL_MODAL: "social_modal",
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitter = createNanoEvents();
|
const emitter = createNanoEvents();
|
||||||
|
|||||||
@ -1,26 +1,88 @@
|
|||||||
import { set } from "lodash";
|
import arrayMove from "array-move";
|
||||||
import React, { createContext, useContext, useReducer } from "react";
|
import { clone, findIndex, get, setWith } from "lodash";
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useReducer,
|
||||||
|
} from "react";
|
||||||
import DatabaseContext from "./DatabaseContext";
|
import DatabaseContext from "./DatabaseContext";
|
||||||
|
|
||||||
const ResumeContext = createContext({});
|
const initialState = {};
|
||||||
|
|
||||||
|
const ResumeContext = createContext(initialState);
|
||||||
|
|
||||||
const ResumeProvider = ({ children }) => {
|
const ResumeProvider = ({ children }) => {
|
||||||
const { debouncedUpdate } = useContext(DatabaseContext);
|
const { debouncedUpdate } = useContext(DatabaseContext);
|
||||||
|
|
||||||
const [state, dispatch] = useReducer((state, { type, payload }) => {
|
const memoizedReducer = useCallback(
|
||||||
let newState;
|
(state, { type, payload }) => {
|
||||||
|
let newState, index, items;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "on_input":
|
case "on_add_item":
|
||||||
newState = set({ ...state }, payload.path, payload.value);
|
items = get(state, payload.path, []);
|
||||||
debouncedUpdate(newState);
|
newState = setWith(
|
||||||
return newState;
|
clone(state),
|
||||||
case "set_data":
|
payload.path,
|
||||||
return payload;
|
[...items, payload.value],
|
||||||
default:
|
clone
|
||||||
throw new Error();
|
);
|
||||||
}
|
debouncedUpdate(newState);
|
||||||
}, {});
|
return newState;
|
||||||
|
|
||||||
|
case "on_edit_item":
|
||||||
|
items = get(state, payload.path);
|
||||||
|
index = findIndex(items, ["id", payload.value.id]);
|
||||||
|
newState = setWith(
|
||||||
|
clone(state),
|
||||||
|
`${payload.path}[${index}]`,
|
||||||
|
payload.value,
|
||||||
|
clone
|
||||||
|
);
|
||||||
|
debouncedUpdate(newState);
|
||||||
|
return newState;
|
||||||
|
|
||||||
|
case "on_delete_item":
|
||||||
|
items = get(state, payload.path);
|
||||||
|
index = findIndex(items, ["id", payload.value.id]);
|
||||||
|
items.splice(index, 1);
|
||||||
|
newState = setWith(clone(state), payload.path, items, clone);
|
||||||
|
debouncedUpdate(newState);
|
||||||
|
return newState;
|
||||||
|
|
||||||
|
case "on_move_item_up":
|
||||||
|
items = get(state, payload.path);
|
||||||
|
index = findIndex(items, ["id", payload.value.id]);
|
||||||
|
items = arrayMove(items, index, index - 1);
|
||||||
|
newState = setWith(clone(state), payload.path, items, clone);
|
||||||
|
debouncedUpdate(newState);
|
||||||
|
return newState;
|
||||||
|
|
||||||
|
case "on_move_item_down":
|
||||||
|
items = get(state, payload.path);
|
||||||
|
index = findIndex(items, ["id", payload.value.id]);
|
||||||
|
items = arrayMove(items, index, index + 1);
|
||||||
|
newState = setWith(clone(state), payload.path, items, clone);
|
||||||
|
debouncedUpdate(newState);
|
||||||
|
return newState;
|
||||||
|
|
||||||
|
case "on_input":
|
||||||
|
newState = setWith(clone(state), payload.path, payload.value, clone);
|
||||||
|
debouncedUpdate(newState);
|
||||||
|
return newState;
|
||||||
|
|
||||||
|
case "set_data":
|
||||||
|
return payload;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[debouncedUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(memoizedReducer, initialState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResumeContext.Provider value={{ state, dispatch }}>
|
<ResumeContext.Provider value={{ state, dispatch }}>
|
||||||
|
|||||||
85
src/contexts/StorageContext.js
Normal file
85
src/contexts/StorageContext.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import firebase from "gatsby-plugin-firebase";
|
||||||
|
import React, { createContext, useContext, useRef } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { isFileImage } from "../utils";
|
||||||
|
import ResumeContext from "./ResumeContext";
|
||||||
|
import UserContext from "./UserContext";
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
uploadPhotograph: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StorageContext = createContext(defaultState);
|
||||||
|
|
||||||
|
const StorageProvider = ({ children }) => {
|
||||||
|
const toastId = useRef(null);
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const { state, dispatch } = useContext(ResumeContext);
|
||||||
|
|
||||||
|
const uploadPhotograph = async (file) => {
|
||||||
|
if (!isFileImage(file)) {
|
||||||
|
toast.error(
|
||||||
|
"You tried to upload a file that was not an image. That won't look good on your resume. Please try again."
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadTask = firebase
|
||||||
|
.storage()
|
||||||
|
.ref(`/users/${user.uid}/photographs/${state.id}`)
|
||||||
|
.put(file);
|
||||||
|
|
||||||
|
let progress = 0;
|
||||||
|
toastId.current = toast("Firing up engines...", {
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadTask.on(
|
||||||
|
"state_changed",
|
||||||
|
(snapshot) => {
|
||||||
|
progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||||
|
toast.update(toastId.current, {
|
||||||
|
render: "Uploading...",
|
||||||
|
progress,
|
||||||
|
hideProgressBar: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error) => toast.error(error),
|
||||||
|
async () => {
|
||||||
|
const downloadURL = await uploadTask.snapshot.ref.getDownloadURL();
|
||||||
|
dispatch({
|
||||||
|
type: "on_input",
|
||||||
|
payload: {
|
||||||
|
path: "profile.photograph",
|
||||||
|
value: downloadURL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.update(toastId.current, {
|
||||||
|
render:
|
||||||
|
"Your photograph was uploaded successfully... and you look great!",
|
||||||
|
progress,
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
toastId.current = null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StorageContext.Provider
|
||||||
|
value={{
|
||||||
|
uploadPhotograph,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StorageContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StorageContext;
|
||||||
|
|
||||||
|
export { StorageProvider };
|
||||||
@ -6,13 +6,13 @@ import useAuthState from "../hooks/useAuthState";
|
|||||||
|
|
||||||
const defaultUser = {
|
const defaultUser = {
|
||||||
uid: null,
|
uid: null,
|
||||||
displayName: null,
|
|
||||||
email: null,
|
email: null,
|
||||||
photoURL: null,
|
displayName: null,
|
||||||
isAnonymous: false,
|
isAnonymous: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
loading: false,
|
||||||
user: defaultUser,
|
user: defaultUser,
|
||||||
logout: async () => {},
|
logout: async () => {},
|
||||||
loginWithGoogle: async () => {},
|
loginWithGoogle: async () => {},
|
||||||
@ -32,8 +32,8 @@ const UserProvider = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (firebaseUser) {
|
if (firebaseUser) {
|
||||||
const user = pick(firebaseUser, Object.keys(defaultUser));
|
const user = pick(firebaseUser, Object.keys(defaultUser));
|
||||||
setUser(user);
|
|
||||||
localStorage.setItem("user", JSON.stringify(user));
|
localStorage.setItem("user", JSON.stringify(user));
|
||||||
|
setUser(user);
|
||||||
|
|
||||||
const addUserToDatabase = async () => {
|
const addUserToDatabase = async () => {
|
||||||
const userRef = firebase.database().ref(`users/${user.uid}`);
|
const userRef = firebase.database().ref(`users/${user.uid}`);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AiOutlineTwitter } from "react-icons/ai";
|
import { AiOutlineTwitter } from "react-icons/ai";
|
||||||
import { MdPerson } from "react-icons/md";
|
import { MdPerson } from "react-icons/md";
|
||||||
import Profile from "../components/builder/sections/Profile";
|
import Profile from "../components/builder/sections/Profile";
|
||||||
import SocialNetwork from "../components/builder/sections/SocialNetwork";
|
import Social from "../components/builder/sections/Social";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -14,6 +14,6 @@ export default [
|
|||||||
id: "social",
|
id: "social",
|
||||||
name: "Social Network",
|
name: "Social Network",
|
||||||
icon: AiOutlineTwitter,
|
icon: AiOutlineTwitter,
|
||||||
component: SocialNetwork,
|
component: Social,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
import { useFormik } from "formik";
|
|
||||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import * as Yup from "yup";
|
|
||||||
import Button from "../components/shared/Button";
|
|
||||||
import Input from "../components/shared/Input";
|
|
||||||
import DatabaseContext from "../contexts/DatabaseContext";
|
|
||||||
import ModalContext from "../contexts/ModalContext";
|
|
||||||
import { getModalText } from "../utils";
|
|
||||||
import BaseModal from "./BaseModal";
|
|
||||||
|
|
||||||
const CreateResumeSchema = Yup.object().shape({
|
|
||||||
name: Yup.string()
|
|
||||||
.min(5, "Please enter at least 5 characters.")
|
|
||||||
.required("This is a required field."),
|
|
||||||
});
|
|
||||||
|
|
||||||
const CreateResumeModal = () => {
|
|
||||||
const modalRef = useRef(null);
|
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [isEditMode, setEditMode] = useState(false);
|
|
||||||
|
|
||||||
const { emitter, events } = useContext(ModalContext);
|
|
||||||
const { createResume, updateResume } = useContext(DatabaseContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unbind = emitter.on(events.CREATE_RESUME_MODAL, (data) => {
|
|
||||||
setOpen(true);
|
|
||||||
setData(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => unbind();
|
|
||||||
}, [emitter, events]);
|
|
||||||
|
|
||||||
const formik = useFormik({
|
|
||||||
initialValues: {
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
validationSchema: CreateResumeSchema,
|
|
||||||
onSubmit: (newData) => {
|
|
||||||
if (isEditMode) {
|
|
||||||
if (data !== newData) {
|
|
||||||
updateResume(newData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createResume(newData);
|
|
||||||
}
|
|
||||||
|
|
||||||
modalRef.current.handleClose();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
data && formik.setValues(data) && setEditMode(true);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const submitAction = (
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
title={getModalText(isEditMode, "Resume")}
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDestroy = () => {
|
|
||||||
formik.resetForm();
|
|
||||||
setEditMode(false);
|
|
||||||
setData(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseModal
|
|
||||||
ref={modalRef}
|
|
||||||
state={[open, setOpen]}
|
|
||||||
title={getModalText(isEditMode, "Resume")}
|
|
||||||
action={submitAction}
|
|
||||||
onDestroy={onDestroy}
|
|
||||||
>
|
|
||||||
<form className="mb-8">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Full Stack Web Developer"
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
value={formik.values.name}
|
|
||||||
error={formik.errors.name}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</BaseModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateResumeModal;
|
|
||||||
108
src/modals/DataModal.js
Normal file
108
src/modals/DataModal.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { isEmpty, isFunction } from "lodash";
|
||||||
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import Button from "../components/shared/Button";
|
||||||
|
import ModalContext from "../contexts/ModalContext";
|
||||||
|
import ResumeContext from "../contexts/ResumeContext";
|
||||||
|
import { getModalText } from "../utils";
|
||||||
|
import BaseModal from "./BaseModal";
|
||||||
|
|
||||||
|
const DataModal = ({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
event,
|
||||||
|
title,
|
||||||
|
formik,
|
||||||
|
onEdit,
|
||||||
|
onCreate,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const modalRef = useRef(null);
|
||||||
|
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isEditMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
|
const { emitter } = useContext(ModalContext);
|
||||||
|
const { dispatch } = useContext(ResumeContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unbind = emitter.on(event, (data) => {
|
||||||
|
setOpen(true);
|
||||||
|
setData(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unbind();
|
||||||
|
}, [emitter, event]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data && formik.setValues(data) && setEditMode(true);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onSubmit = async (newData) => {
|
||||||
|
if (isEmpty(await formik.validateForm(newData))) {
|
||||||
|
if (isEditMode) {
|
||||||
|
if (data !== newData) {
|
||||||
|
isFunction(onEdit)
|
||||||
|
? onEdit(newData)
|
||||||
|
: dispatch({
|
||||||
|
type: "on_edit_item",
|
||||||
|
payload: {
|
||||||
|
path,
|
||||||
|
value: newData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newData.id = uuidv4();
|
||||||
|
|
||||||
|
isFunction(onCreate)
|
||||||
|
? onCreate(newData)
|
||||||
|
: dispatch({
|
||||||
|
type: "on_add_item",
|
||||||
|
payload: {
|
||||||
|
path,
|
||||||
|
value: newData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modalRef.current.handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = isEmpty(title)
|
||||||
|
? getModalText(isEditMode, name)
|
||||||
|
: isEditMode
|
||||||
|
? title.edit
|
||||||
|
: title.create;
|
||||||
|
|
||||||
|
const submitAction = (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
title={getTitle}
|
||||||
|
onClick={() => onSubmit(formik.values)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDestroy = () => {
|
||||||
|
formik.resetForm();
|
||||||
|
setEditMode(false);
|
||||||
|
setData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal
|
||||||
|
ref={modalRef}
|
||||||
|
action={submitAction}
|
||||||
|
onDestroy={onDestroy}
|
||||||
|
state={[open, setOpen]}
|
||||||
|
title={getTitle}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataModal;
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import AuthModal from "./AuthModal";
|
import AuthModal from "./AuthModal";
|
||||||
import CreateResumeModal from "./CreateResumeModal";
|
import ResumeModal from "./ResumeModal";
|
||||||
|
import SocialModal from "./sections/SocialModal";
|
||||||
|
|
||||||
const ModalRegistrar = () => {
|
const ModalRegistrar = () => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<AuthModal />
|
<AuthModal />
|
||||||
<CreateResumeModal />
|
<ResumeModal />
|
||||||
|
<SocialModal />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
64
src/modals/ResumeModal.js
Normal file
64
src/modals/ResumeModal.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useFormik } from "formik";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Input from "../components/shared/Input";
|
||||||
|
import DatabaseContext from "../contexts/DatabaseContext";
|
||||||
|
import ModalContext from "../contexts/ModalContext";
|
||||||
|
import DataModal from "./DataModal";
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
name: Yup.string()
|
||||||
|
.min(5, "Please enter at least 5 characters.")
|
||||||
|
.required("This is a required field."),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ResumeModal = () => {
|
||||||
|
const { events } = useContext(ModalContext);
|
||||||
|
const { createResume, updateResume } = useContext(DatabaseContext);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
validationSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getFieldProps = (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}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
className="mb-8"
|
||||||
|
placeholder="Full Stack Web Developer"
|
||||||
|
{...getFieldProps("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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResumeModal;
|
||||||
77
src/modals/sections/SocialModal.js
Normal file
77
src/modals/sections/SocialModal.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useFormik } from "formik";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Input from "../../components/shared/Input";
|
||||||
|
import ModalContext from "../../contexts/ModalContext";
|
||||||
|
import DataModal from "../DataModal";
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
url: "https://",
|
||||||
|
network: "",
|
||||||
|
username: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
network: Yup.string()
|
||||||
|
.min(5, "Please enter at least 5 characters.")
|
||||||
|
.required("This is a required field."),
|
||||||
|
username: Yup.string().required("This is a required field."),
|
||||||
|
url: Yup.string()
|
||||||
|
.min(5, "Please enter at least 5 characters.")
|
||||||
|
.required("This is a required field.")
|
||||||
|
.url("Must be a valid URL"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const SocialModal = () => {
|
||||||
|
const { events } = useContext(ModalContext);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
validationSchema,
|
||||||
|
validateOnBlur: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getFieldProps = (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}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-8">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="network"
|
||||||
|
label="Network"
|
||||||
|
placeholder="Twitter"
|
||||||
|
{...getFieldProps("network")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
placeholder="KingOKings"
|
||||||
|
{...getFieldProps("username")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="url"
|
||||||
|
label="URL"
|
||||||
|
className="col-span-2"
|
||||||
|
placeholder="https://twitter.com/KingOKings"
|
||||||
|
{...getFieldProps("url")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DataModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SocialModal;
|
||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { navigate } from "gatsby";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import Artboard from "../../components/builder/center/Artboard";
|
import Artboard from "../../components/builder/center/Artboard";
|
||||||
import LeftSidebar from "../../components/builder/left/LeftSidebar";
|
import LeftSidebar from "../../components/builder/left/LeftSidebar";
|
||||||
import RightSidebar from "../../components/builder/right/RightSidebar";
|
import RightSidebar from "../../components/builder/right/RightSidebar";
|
||||||
@ -6,7 +8,7 @@ import LoadingScreen from "../../components/router/LoadingScreen";
|
|||||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||||
import ResumeContext from "../../contexts/ResumeContext";
|
import ResumeContext from "../../contexts/ResumeContext";
|
||||||
|
|
||||||
const Builder = ({ user, id }) => {
|
const Builder = ({ id }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { getResume } = useContext(DatabaseContext);
|
const { getResume } = useContext(DatabaseContext);
|
||||||
const { dispatch } = useContext(ResumeContext);
|
const { dispatch } = useContext(ResumeContext);
|
||||||
@ -14,13 +16,23 @@ const Builder = ({ user, id }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const resume = await getResume(id);
|
const resume = await getResume(id);
|
||||||
|
|
||||||
|
if (!resume) {
|
||||||
|
navigate("/app/dashboard");
|
||||||
|
toast.error(
|
||||||
|
`The resume you were looking for does not exist anymore... or maybe it never did?`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({ type: "set_data", payload: resume });
|
dispatch({ type: "set_data", payload: resume });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})();
|
})();
|
||||||
}, [id, getResume, dispatch]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingScreen message="Loading Resume..." />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import firebase from "gatsby-plugin-firebase";
|
import firebase from "gatsby-plugin-firebase";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
import CreateResume from "../../components/dashboard/CreateResume";
|
import CreateResume from "../../components/dashboard/CreateResume";
|
||||||
import ResumePreview from "../../components/dashboard/ResumePreview";
|
import ResumePreview from "../../components/dashboard/ResumePreview";
|
||||||
import TopNavbar from "../../components/dashboard/TopNavbar";
|
import TopNavbar from "../../components/dashboard/TopNavbar";
|
||||||
@ -10,26 +11,38 @@ const Dashboard = ({ user }) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const ref = `users/${user.uid}/resumes`;
|
||||||
|
|
||||||
firebase
|
firebase
|
||||||
.database()
|
.database()
|
||||||
.ref(`users/${user.uid}/resumes`)
|
.ref(ref)
|
||||||
.on("value", (snapshot) => {
|
.on("value", (snapshot) => {
|
||||||
if (snapshot.val()) {
|
if (snapshot.val()) {
|
||||||
const resumes = [];
|
const resumes = [];
|
||||||
const data = snapshot.val();
|
const data = snapshot.val();
|
||||||
Object.keys(data).forEach((key) => resumes.push(data[key]));
|
Object.keys(data).forEach((key) => resumes.push(data[key]));
|
||||||
setResumes(resumes);
|
setResumes(resumes);
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
firebase.database().ref(ref).off();
|
||||||
|
};
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingScreen message="Connecting to database..." />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>Dashboard | Reactive Resume</title>
|
||||||
|
<link rel="canonical" href="https://rxresu.me/app/dashboard" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<TopNavbar />
|
<TopNavbar />
|
||||||
|
|
||||||
<div className="container mt-12">
|
<div className="container mt-12">
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Wrapper from "../components/shared/Wrapper";
|
import { Helmet } from "react-helmet";
|
||||||
import Hero from "../components/landing/Hero";
|
import Hero from "../components/landing/Hero";
|
||||||
|
import Wrapper from "../components/shared/Wrapper";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
<Helmet>
|
||||||
|
<title>Reactive Resume</title>
|
||||||
|
<link rel="canonical" href="https://rxresu.me/" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="container mt-24">
|
<div className="container mt-24">
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@apply text-justify leading-loose;
|
@apply leading-loose;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -41,6 +41,10 @@ section {
|
|||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiMenuItem-root {
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
.spin {
|
.spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export const getModalText = (isEditMode, type) => {
|
export const getModalText = (isEditMode, type) => {
|
||||||
return isEditMode ? `Edit ${type}` : `Create New ${type}`;
|
return isEditMode ? `Edit ${type}` : `Add ${type}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformCollectionSnapshot = (snapshot, setData) => {
|
export const transformCollectionSnapshot = (snapshot, setData) => {
|
||||||
@ -7,3 +7,12 @@ export const transformCollectionSnapshot = (snapshot, setData) => {
|
|||||||
snapshot.forEach((doc) => data.push(doc.data()));
|
snapshot.forEach((doc) => data.push(doc.data()));
|
||||||
setData(data);
|
setData(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleKeyDown = (event, action) => {
|
||||||
|
event.which === 13 && action();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFileImage = (file) => {
|
||||||
|
const acceptedImageTypes = ["image/jpeg", "image/png"];
|
||||||
|
return file && acceptedImageTypes.includes(file["type"]);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user