- creating and updating resumes

This commit is contained in:
Amruth Pillai 2020-07-04 14:31:25 +05:30
parent e1f1d84201
commit e247cb102c
16 changed files with 273 additions and 72 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"files.associations": {
"*.js": "javascriptreact"
}
}

View File

@ -10,6 +10,7 @@ import { UserProvider } from "./src/contexts/UserContext";
import "./src/styles/colors.css";
import "./src/styles/tailwind.css";
import "./src/styles/global.css";
import { ResumeProvider } from "./src/contexts/ResumeContext";
const theme = createMuiTheme({
typography: {
@ -22,7 +23,9 @@ export const wrapRootElement = ({ element }) => (
<ThemeProvider>
<MuiThemeProvider theme={theme}>
<ModalProvider>
<UserProvider>{element}</UserProvider>
<UserProvider>
<ResumeProvider>{element}</ResumeProvider>
</UserProvider>
</ModalProvider>
</MuiThemeProvider>
</ThemeProvider>

34
package-lock.json generated
View File

@ -7953,6 +7953,11 @@
"readable-stream": "^2.3.6"
}
},
"fn-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz",
"integrity": "sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA=="
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
@ -14693,6 +14698,11 @@
"signal-exit": "^3.0.2"
}
},
"property-expr": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz",
"integrity": "sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g=="
},
"property-information": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.5.0.tgz",
@ -17465,6 +17475,11 @@
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"synchronous-promise": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz",
"integrity": "sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA=="
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@ -17923,6 +17938,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -19667,6 +19687,20 @@
"@types/yoga-layout": "1.9.2"
}
},
"yup": {
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.29.1.tgz",
"integrity": "sha512-U7mPIbgfQWI6M3hZCJdGFrr+U0laG28FxMAKIgNvgl7OtyYuUoc4uy9qCWYHZjh49b8T7Ug8NNDdiMIEytcXrQ==",
"requires": {
"@babel/runtime": "^7.9.6",
"fn-name": "~3.0.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.11",
"property-expr": "^2.0.2",
"synchronous-promise": "^2.0.10",
"toposort": "^2.0.2"
}
},
"yurnalist": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/yurnalist/-/yurnalist-1.1.2.tgz",

View File

@ -31,6 +31,7 @@
"gatsby-source-filesystem": "^2.3.18",
"gatsby-transformer-sharp": "^2.5.10",
"lodash": "^4.17.15",
"moment": "^2.27.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-firebase-hooks": "^2.2.0",
@ -38,7 +39,8 @@
"react-icons": "^3.10.0",
"react-loader-spinner": "^3.1.14",
"react-toastify": "^6.0.8",
"uuid": "^8.2.0"
"uuid": "^8.2.0",
"yup": "^0.29.1"
},
"devDependencies": {
"prettier": "2.0.5",

View File

@ -1,7 +1,7 @@
import React, { useContext } from "react";
import { MdAdd } from "react-icons/md";
import styles from "./CreateResume.module.css";
import ModalContext from "../../contexts/ModalContext";
import styles from "./CreateResume.module.css";
const CreateResume = () => {
const { createResumeModal } = useContext(ModalContext);

View File

@ -1,20 +1,28 @@
import React, { useState } from "react";
import { MdMoreHoriz, MdOpenInNew } from "react-icons/md";
import { Menu, MenuItem } from "@material-ui/core";
import moment from "moment";
import React, { useContext, useState } from "react";
import { MdMoreHoriz, MdOpenInNew } from "react-icons/md";
import ModalContext from "../../contexts/ModalContext";
import styles from "./ResumePreview.module.css";
const ResumePreview = ({ title, subtitle }) => {
const ResumePreview = ({ resume }) => {
const [anchorEl, setAnchorEl] = useState(null);
const { createResumeModal } = useContext(ModalContext);
const handleClick = () => {
const handleOpen = () => {
console.log("Hello, World!");
};
const handleMenuClick = (event) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const handleRename = () => {
createResumeModal.setOpen(true);
createResumeModal.setData(resume);
setAnchorEl(null);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
@ -32,7 +40,7 @@ const ResumePreview = ({ title, subtitle }) => {
color="#fff"
size="48"
className="cursor-pointer"
onClick={handleClick}
onClick={handleOpen}
/>
<MdMoreHoriz
color="#fff"
@ -47,6 +55,7 @@ const ResumePreview = ({ title, subtitle }) => {
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleRename}>Rename</MenuItem>
<MenuItem onClick={handleMenuClose}>Duplicate</MenuItem>
<MenuItem onClick={handleMenuClose}>
<span className="text-red-600">Delete</span>
@ -54,8 +63,12 @@ const ResumePreview = ({ title, subtitle }) => {
</Menu>
</div>
<div className={styles.meta}>
<p>{title}</p>
<span>{subtitle}</span>
<p>{resume.name}</p>
{resume.updatedAt && (
<span>
Last updated {moment(resume.updatedAt.toDate()).fromNow()}
</span>
)}
</div>
</div>
);

View File

@ -28,7 +28,7 @@
.resume > .meta {
margin-top: 260px;
@apply flex flex-col items-center;
@apply flex flex-col text-center items-center;
}
.resume > .meta p {

View File

@ -3,7 +3,15 @@ import classNames from "classnames";
import Loader from "react-loader-spinner";
import styles from "./Button.module.css";
const Button = ({ icon, title, isLoading, onClick, outline, className }) => {
const Button = ({
icon,
title,
onClick,
outline,
className,
isLoading,
type = "button",
}) => {
const Icon = icon;
const classes = classNames(styles.container, className, {
[styles.outline]: outline,
@ -12,9 +20,8 @@ const Button = ({ icon, title, isLoading, onClick, outline, className }) => {
const handleKeyDown = () => {};
return (
<div
tabIndex="0"
role="button"
<button
type={type}
className={classes}
onKeyDown={handleKeyDown}
onClick={isLoading ? undefined : onClick}
@ -25,7 +32,7 @@ const Button = ({ icon, title, isLoading, onClick, outline, className }) => {
) : (
title
)}
</div>
</button>
);
};

View File

@ -6,6 +6,7 @@ const Input = ({
label,
name,
value,
error,
onChange,
placeholder,
type = "text",
@ -24,6 +25,7 @@ const Input = ({
onChange={onChange}
placeholder={placeholder}
/>
<p className="mt-1 text-red-600 text-sm">{error}</p>
</label>
</div>
);

View File

@ -1,8 +1,16 @@
import React, { createContext, useState } from "react";
const defaultState = {
authModal: {},
createResumeModal: {},
authModal: {
isOpen: false,
setOpen: () => {},
},
createResumeModal: {
isOpen: false,
setOpen: () => {},
data: null,
setData: () => {},
},
};
const ModalContext = createContext(defaultState);
@ -10,6 +18,7 @@ const ModalContext = createContext(defaultState);
const ModalProvider = ({ children }) => {
const [authOpen, setAuthOpen] = useState(false);
const [createResumeOpen, setCreateResumeOpen] = useState(false);
const [createResumeData, setCreateResumeData] = useState(null);
return (
<ModalContext.Provider
@ -18,6 +27,8 @@ const ModalProvider = ({ children }) => {
createResumeModal: {
isOpen: createResumeOpen,
setOpen: setCreateResumeOpen,
data: createResumeData,
setData: setCreateResumeData,
},
}}
>

View File

@ -0,0 +1,74 @@
import firebase from "gatsby-plugin-firebase";
import React, { createContext, useContext, useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { transformCollectionSnapshot } from "../utils";
import UserContext from "./UserContext";
const defaultState = {
resumes: [],
createResume: async () => {},
};
const ResumeContext = createContext(defaultState);
const ResumeProvider = ({ children }) => {
const [resumes, setResumes] = useState([null]);
const [collectionRef, setCollectionRef] = useState(null);
const { user } = useContext(UserContext);
useEffect(() => {
if (user) {
setCollectionRef(`users/${user.uid}/resumes`);
}
}, [user]);
useEffect(() => {
if (collectionRef) {
firebase
.firestore()
.collection(collectionRef)
.onSnapshot((snapshot) =>
transformCollectionSnapshot(snapshot, setResumes)
);
}
}, [collectionRef]);
const createResume = async ({ name }) => {
const id = uuidv4();
const createdAt = firebase.firestore.FieldValue.serverTimestamp();
await firebase.firestore().collection(collectionRef).doc(id).set({
id,
name,
createdAt,
updatedAt: createdAt,
});
};
const updateResume = async (resume) => {
const { id, name } = resume;
if (resumes.find((x) => x.id === id) === resume) return;
await firebase.firestore().collection(collectionRef).doc(id).update({
id,
name,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
});
};
return (
<ResumeContext.Provider
value={{
resumes,
createResume,
updateResume,
}}
>
{children}
</ResumeContext.Provider>
);
};
export default ResumeContext;
export { ResumeProvider };

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { forwardRef, useImperativeHandle } from "react";
import { isFunction } from "lodash";
import Modal from "@material-ui/core/Modal";
import Backdrop from "@material-ui/core/Backdrop";
@ -7,45 +7,53 @@ import { MdClose } from "react-icons/md";
import styles from "./BaseModal.module.css";
import Button from "../components/shared/Button";
const BaseModal = ({ title, state, children, action, onDestroy }) => {
const { isOpen, setOpen } = state;
const BaseModal = forwardRef(
({ title, state, children, action, onDestroy }, ref) => {
const { isOpen, setOpen, setData } = state;
const handleClose = () => {
setOpen(false);
isFunction(onDestroy) && onDestroy();
};
const handleClose = () => {
setOpen(false);
return (
<Modal
open={isOpen}
onClose={handleClose}
closeAfterTransition
className={styles.root}
BackdropComponent={Backdrop}
>
<Fade in={isOpen}>
<div className={styles.modal}>
<div className={styles.title}>
<h2>{title}</h2>
<MdClose size="18" onClick={handleClose} />
setTimeout(() => {
isFunction(setData) && setData(null);
isFunction(onDestroy) && onDestroy();
}, 250);
};
useImperativeHandle(ref, () => ({ handleClose }));
return (
<Modal
open={isOpen}
closeAfterTransition
onClose={handleClose}
className={styles.root}
BackdropComponent={Backdrop}
>
<Fade in={isOpen}>
<div className={styles.modal}>
<div className={styles.title}>
<h2>{title}</h2>
<MdClose size="18" onClick={handleClose} />
</div>
<div className={styles.body}>{children}</div>
<div className={styles.actions}>
<Button
outline
title="Cancel"
className="mr-8"
onClick={handleClose}
/>
{action}
</div>
</div>
<div className={styles.body}>{children}</div>
<div className={styles.actions}>
<Button
outline
title="Cancel"
className="mr-8"
onClick={handleClose}
/>
{action}
</div>
</div>
</Fade>
</Modal>
);
};
</Fade>
</Modal>
);
}
);
export default BaseModal;

View File

@ -1,33 +1,57 @@
import React, { useContext } from "react";
import { useFormik } from "formik";
import BaseModal from "./BaseModal";
import ModalContext from "../contexts/ModalContext";
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 ModalContext from "../contexts/ModalContext";
import ResumeContext from "../contexts/ResumeContext";
import { getModalText } from "../utils";
import BaseModal from "./BaseModal";
const CreateResumeModal = () => {
const CreateResumeSchema = Yup.object().shape({
name: Yup.string().min(2).required(),
});
const CreateResumeModal = ({ data }) => {
const modalRef = useRef(null);
const [isEditMode, setEditMode] = useState(false);
const { createResumeModal } = useContext(ModalContext);
const { createResume, updateResume } = useContext(ResumeContext);
const formik = useFormik({
initialValues: {
name: "",
},
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
validationSchema: CreateResumeSchema,
onSubmit: (data) => {
isEditMode ? updateResume(data) : createResume(data);
modalRef.current.handleClose();
},
});
useEffect(() => {
data && formik.setValues(data) && setEditMode(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const submitAction = (
<Button title="Create Resume" onClick={() => formik.handleSubmit()} />
<Button
type="submit"
title={getModalText(isEditMode, "Resume")}
onClick={() => formik.handleSubmit()}
/>
);
const onDestroy = () => {
formik.resetForm();
setEditMode(false);
};
return (
<BaseModal
ref={modalRef}
state={createResumeModal}
title="Create New Resume"
title={getModalText(isEditMode, "Resume")}
action={submitAction}
onDestroy={onDestroy}
>
@ -39,6 +63,7 @@ const CreateResumeModal = () => {
placeholder="Full Stack Web Developer"
onChange={formik.handleChange}
value={formik.values.name}
error={formik.errors.name}
/>
</form>

View File

@ -1,12 +1,15 @@
import React, { Fragment } from "react";
import React, { Fragment, useContext } from "react";
import AuthModal from "./AuthModal";
import CreateResumeModal from "./CreateResumeModal";
import ModalContext from "../contexts/ModalContext";
const ModalRegistrar = () => {
const { createResumeModal } = useContext(ModalContext);
return (
<Fragment>
<AuthModal />
<CreateResumeModal />
<CreateResumeModal data={createResumeModal.data} />
</Fragment>
);
};

View File

@ -1,10 +1,13 @@
import React from "react";
import React, { useContext } from "react";
import Wrapper from "../../components/shared/Wrapper";
import CreateResume from "../../components/dashboard/CreateResume";
import ResumePreview from "../../components/dashboard/ResumePreview";
import TopNavbar from "../../components/dashboard/TopNavbar";
import ResumeContext from "../../contexts/ResumeContext";
const Dashboard = () => {
const { resumes } = useContext(ResumeContext);
return (
<Wrapper>
<TopNavbar />
@ -12,10 +15,12 @@ const Dashboard = () => {
<div className="container mt-12">
<div className="grid grid-cols-6 gap-8">
<CreateResume />
<ResumePreview
title="Full Stack Developer"
subtitle="Last updated 6 days ago"
/>
{resumes
.filter((x) => x !== null)
.map((x) => (
<ResumePreview key={x.id} resume={x} />
))}
</div>
</div>
</Wrapper>

9
src/utils/index.js Normal file
View File

@ -0,0 +1,9 @@
export const getModalText = (isEditMode, type) => {
return isEditMode ? `Edit ${type}` : `Create New ${type}`;
};
export const transformCollectionSnapshot = (snapshot, setData) => {
const data = [];
snapshot.forEach((doc) => data.push(doc.data()));
setData(data);
};