- 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

@ -2,10 +2,12 @@ import { createMuiTheme, MuiThemeProvider } from "@material-ui/core";
import "firebase/analytics";
import "firebase/auth";
import "firebase/database";
import "firebase/storage";
import React from "react";
import { DatabaseProvider } from "./src/contexts/DatabaseContext";
import { ModalProvider } from "./src/contexts/ModalContext";
import { ResumeProvider } from "./src/contexts/ResumeContext";
import { StorageProvider } from "./src/contexts/StorageContext";
import { TemplateProvider } from "./src/contexts/TemplateContext";
import { ThemeProvider } from "./src/contexts/ThemeContext";
import { UserProvider } from "./src/contexts/UserContext";
@ -29,7 +31,9 @@ export const wrapRootElement = ({ element }) => (
<UserProvider>
<DatabaseProvider>
<ResumeProvider>
<TemplateProvider>{element}</TemplateProvider>
<StorageProvider>
<TemplateProvider>{element}</TemplateProvider>
</StorageProvider>
</ResumeProvider>
</DatabaseProvider>
</UserProvider>

View File

@ -2,6 +2,7 @@ require("dotenv").config();
module.exports = {
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-prefetch-google-fonts`,
options: {
@ -24,7 +25,6 @@ module.exports = {
},
},
`gatsby-plugin-lodash`,
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-manifest`,
options: {

View File

@ -1,3 +1,4 @@
import "firebase/analytics";
import "firebase/auth";
import "firebase/database";
import "firebase/storage";

10
package-lock.json generated
View File

@ -3552,6 +3552,11 @@
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
"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": {
"version": "0.0.0",
"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": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-1.3.18.tgz",

View File

@ -15,6 +15,7 @@
},
"dependencies": {
"@material-ui/core": "^4.11.0",
"array-move": "^2.2.2",
"classnames": "^2.2.6",
"firebase": "^7.15.5",
"formik": "^2.1.4",
@ -30,6 +31,7 @@
"gatsby-plugin-react-helmet": "^3.3.9",
"gatsby-plugin-sharp": "^2.6.18",
"gatsby-source-filesystem": "^2.3.18",
"gatsby-source-gravatar": "^1.0.0",
"gatsby-transformer-sharp": "^2.5.10",
"lodash": "^4.17.15",
"moment": "^2.27.0",

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>
);

View File

@ -1,7 +1,6 @@
import firebase from "gatsby-plugin-firebase";
import { debounce } from "lodash";
import React, { createContext, useContext, useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import UserContext from "./UserContext";
const defaultState = {
@ -38,27 +37,22 @@ const DatabaseProvider = ({ children }) => {
};
const createResume = (resume) => {
const id = uuidv4();
const { id } = resume;
const createdAt = firebase.database.ServerValue.TIMESTAMP;
let firstName = "",
lastName = "",
photograph = "";
let firstName, lastName;
if (!user.isAnonymous) {
[firstName, lastName] = user.displayName.split(" ");
photograph = user.photoURL;
}
firebase
.database()
.ref(`users/${user.uid}/resumes/${id}`)
.set({
id,
profile: {
firstName,
lastName,
photograph,
firstName: firstName || "",
lastName: lastName || "",
},
...resume,
createdAt,

View File

@ -4,6 +4,7 @@ import React, { createContext } from "react";
const events = {
AUTH_MODAL: "auth_modal",
CREATE_RESUME_MODAL: "create_resume_modal",
SOCIAL_MODAL: "social_modal",
};
const emitter = createNanoEvents();

View File

@ -1,26 +1,88 @@
import { set } from "lodash";
import React, { createContext, useContext, useReducer } from "react";
import arrayMove from "array-move";
import { clone, findIndex, get, setWith } from "lodash";
import React, {
createContext,
useCallback,
useContext,
useReducer,
} from "react";
import DatabaseContext from "./DatabaseContext";
const ResumeContext = createContext({});
const initialState = {};
const ResumeContext = createContext(initialState);
const ResumeProvider = ({ children }) => {
const { debouncedUpdate } = useContext(DatabaseContext);
const [state, dispatch] = useReducer((state, { type, payload }) => {
let newState;
const memoizedReducer = useCallback(
(state, { type, payload }) => {
let newState, index, items;
switch (type) {
case "on_input":
newState = set({ ...state }, payload.path, payload.value);
debouncedUpdate(newState);
return newState;
case "set_data":
return payload;
default:
throw new Error();
}
}, {});
switch (type) {
case "on_add_item":
items = get(state, payload.path, []);
newState = setWith(
clone(state),
payload.path,
[...items, payload.value],
clone
);
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 (
<ResumeContext.Provider value={{ state, dispatch }}>

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

View File

@ -6,13 +6,13 @@ import useAuthState from "../hooks/useAuthState";
const defaultUser = {
uid: null,
displayName: null,
email: null,
photoURL: null,
displayName: null,
isAnonymous: false,
};
const defaultState = {
loading: false,
user: defaultUser,
logout: async () => {},
loginWithGoogle: async () => {},
@ -32,8 +32,8 @@ const UserProvider = ({ children }) => {
useEffect(() => {
if (firebaseUser) {
const user = pick(firebaseUser, Object.keys(defaultUser));
setUser(user);
localStorage.setItem("user", JSON.stringify(user));
setUser(user);
const addUserToDatabase = async () => {
const userRef = firebase.database().ref(`users/${user.uid}`);

View File

@ -1,7 +1,7 @@
import { AiOutlineTwitter } from "react-icons/ai";
import { MdPerson } from "react-icons/md";
import Profile from "../components/builder/sections/Profile";
import SocialNetwork from "../components/builder/sections/SocialNetwork";
import Social from "../components/builder/sections/Social";
export default [
{
@ -14,6 +14,6 @@ export default [
id: "social",
name: "Social Network",
icon: AiOutlineTwitter,
component: SocialNetwork,
component: Social,
},
];

View File

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

View File

@ -1,12 +1,14 @@
import React, { Fragment } from "react";
import AuthModal from "./AuthModal";
import CreateResumeModal from "./CreateResumeModal";
import ResumeModal from "./ResumeModal";
import SocialModal from "./sections/SocialModal";
const ModalRegistrar = () => {
return (
<Fragment>
<AuthModal />
<CreateResumeModal />
<ResumeModal />
<SocialModal />
</Fragment>
);
};

64
src/modals/ResumeModal.js Normal file
View 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;

View 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;

View File

@ -1,4 +1,6 @@
import { navigate } from "gatsby";
import React, { useContext, useEffect, useState } from "react";
import { toast } from "react-toastify";
import Artboard from "../../components/builder/center/Artboard";
import LeftSidebar from "../../components/builder/left/LeftSidebar";
import RightSidebar from "../../components/builder/right/RightSidebar";
@ -6,7 +8,7 @@ import LoadingScreen from "../../components/router/LoadingScreen";
import DatabaseContext from "../../contexts/DatabaseContext";
import ResumeContext from "../../contexts/ResumeContext";
const Builder = ({ user, id }) => {
const Builder = ({ id }) => {
const [loading, setLoading] = useState(true);
const { getResume } = useContext(DatabaseContext);
const { dispatch } = useContext(ResumeContext);
@ -14,13 +16,23 @@ const Builder = ({ user, id }) => {
useEffect(() => {
(async () => {
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 });
setLoading(false);
})();
}, [id, getResume, dispatch]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
if (loading) {
return <LoadingScreen message="Loading Resume..." />;
return <LoadingScreen />;
}
return (

View File

@ -1,5 +1,6 @@
import firebase from "gatsby-plugin-firebase";
import React, { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import CreateResume from "../../components/dashboard/CreateResume";
import ResumePreview from "../../components/dashboard/ResumePreview";
import TopNavbar from "../../components/dashboard/TopNavbar";
@ -10,26 +11,38 @@ const Dashboard = ({ user }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const ref = `users/${user.uid}/resumes`;
firebase
.database()
.ref(`users/${user.uid}/resumes`)
.ref(ref)
.on("value", (snapshot) => {
if (snapshot.val()) {
const resumes = [];
const data = snapshot.val();
Object.keys(data).forEach((key) => resumes.push(data[key]));
setResumes(resumes);
setLoading(false);
}
setLoading(false);
});
return () => {
firebase.database().ref(ref).off();
};
}, [user]);
if (loading) {
return <LoadingScreen message="Connecting to database..." />;
return <LoadingScreen />;
}
return (
<div>
<Helmet>
<title>Dashboard | Reactive Resume</title>
<link rel="canonical" href="https://rxresu.me/app/dashboard" />
</Helmet>
<TopNavbar />
<div className="container mt-12">

View File

@ -1,10 +1,16 @@
import React from "react";
import Wrapper from "../components/shared/Wrapper";
import { Helmet } from "react-helmet";
import Hero from "../components/landing/Hero";
import Wrapper from "../components/shared/Wrapper";
const Home = () => {
return (
<Wrapper>
<Helmet>
<title>Reactive Resume</title>
<link rel="canonical" href="https://rxresu.me/" />
</Helmet>
<div className="container mt-24">
<Hero />

View File

@ -10,7 +10,7 @@ body {
}
p {
@apply text-justify leading-loose;
@apply leading-loose;
}
a {
@ -41,6 +41,10 @@ section {
font-size: 10px !important;
}
.MuiMenuItem-root {
justify-content: center !important;
}
.spin {
animation: spin 1s linear infinite;
}

View File

@ -1,5 +1,5 @@
export const getModalText = (isEditMode, type) => {
return isEditMode ? `Edit ${type}` : `Create New ${type}`;
return isEditMode ? `Edit ${type}` : `Add ${type}`;
};
export const transformCollectionSnapshot = (snapshot, setData) => {
@ -7,3 +7,12 @@ export const transformCollectionSnapshot = (snapshot, setData) => {
snapshot.forEach((doc) => data.push(doc.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"]);
};