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/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>
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import "firebase/analytics";
|
||||
import "firebase/auth";
|
||||
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",
|
||||
"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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import ResumeContext from "../../../contexts/ResumeContext";
|
||||
import TemplateContext from "../../../contexts/TemplateContext";
|
||||
import Onyx from "../../../templates/Onyx";
|
||||
@ -9,8 +10,18 @@ const Artboard = () => {
|
||||
const { state } = useContext(ResumeContext);
|
||||
|
||||
return (
|
||||
<div id="artboard" className={styles.container}>
|
||||
<Onyx data={state} layout={blocks} colors={colors} />
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{state.name} | Reactive Resume</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`https://rxresu.me/app/builder/${state.id}`}
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
<div id="artboard" className={styles.container}>
|
||||
<Onyx data={state} layout={blocks} colors={colors} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React, { Fragment } from "react";
|
||||
import React, { Fragment, useContext } from "react";
|
||||
import ResumeContext from "../../../contexts/ResumeContext";
|
||||
import sections from "../../../data/leftSections";
|
||||
import LeftNavbar from "./LeftNavbar";
|
||||
import styles from "./LeftSidebar.module.css";
|
||||
|
||||
const LeftSidebar = () => {
|
||||
const { state } = useContext(ResumeContext);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<LeftNavbar />
|
||||
@ -11,7 +14,7 @@ const LeftSidebar = () => {
|
||||
<div className={styles.container}>
|
||||
{sections.map(({ id, component: Component }) => (
|
||||
<Fragment key={id}>
|
||||
<Component />
|
||||
<Component state={state} />
|
||||
<hr />
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
9
src/components/builder/lists/EmptyList.js
Normal file
9
src/components/builder/lists/EmptyList.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const EmptyList = () => (
|
||||
<div className="rounded border border-secondary py-6 text-center">
|
||||
This list is empty.
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EmptyList;
|
||||
26
src/components/builder/lists/List.js
Normal file
26
src/components/builder/lists/List.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import React from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import Button from "../../shared/Button";
|
||||
import EmptyList from "./EmptyList";
|
||||
import styles from "./List.module.css";
|
||||
|
||||
const List = ({ items, onAdd, children }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={styles.container}>
|
||||
{isEmpty(items) ? <EmptyList /> : children}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
outline
|
||||
icon={MdAdd}
|
||||
title="Add New"
|
||||
onClick={onAdd}
|
||||
className="mt-8 ml-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
||||
3
src/components/builder/lists/List.module.css
Normal file
3
src/components/builder/lists/List.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.container {
|
||||
@apply flex flex-col border border-secondary rounded;
|
||||
}
|
||||
103
src/components/builder/lists/small/SmallListItem.js
Normal file
103
src/components/builder/lists/small/SmallListItem.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { Menu, MenuItem } from "@material-ui/core";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io";
|
||||
import { MdMoreVert } from "react-icons/md";
|
||||
import ResumeContext from "../../../../contexts/ResumeContext";
|
||||
import styles from "./SmallListItem.module.css";
|
||||
|
||||
const SmallListItem = ({
|
||||
title,
|
||||
subtitle,
|
||||
path,
|
||||
data,
|
||||
isFirst,
|
||||
isLast,
|
||||
onEdit,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const { dispatch } = useContext(ResumeContext);
|
||||
|
||||
const handleClick = (event) => setAnchorEl(event.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveUp = () => {
|
||||
dispatch({
|
||||
type: "on_move_item_up",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
dispatch({
|
||||
type: "on_move_item_down",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch({
|
||||
type: "on_delete_item",
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{title}</span>
|
||||
<span className="mt-1 text-sm opacity-75">{subtitle}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.menu}>
|
||||
<MdMoreVert
|
||||
size="18px"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
open={Boolean(anchorEl)}
|
||||
>
|
||||
<div className="flex items-center space-around">
|
||||
<MenuItem disabled={isFirst} onClick={handleMoveUp}>
|
||||
<IoIosArrowUp size="18px" />
|
||||
</MenuItem>
|
||||
<MenuItem disabled={isLast} onClick={handleMoveDown}>
|
||||
<IoIosArrowDown size="18px" />
|
||||
</MenuItem>
|
||||
</div>
|
||||
<MenuItem onClick={handleEdit}>Edit</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<span className="text-red-600 font-medium">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallListItem;
|
||||
17
src/components/builder/lists/small/SmallListItem.module.css
Normal file
17
src/components/builder/lists/small/SmallListItem.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
@apply flex items-center justify-between border-t border-secondary px-8 py-5;
|
||||
}
|
||||
|
||||
.container:first-child {
|
||||
@apply border-t-0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@apply opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.container:hover .menu {
|
||||
@apply opacity-100;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
@ -1,19 +1,45 @@
|
||||
import React from "react";
|
||||
import React, { useContext, useRef } from "react";
|
||||
import { MdFileUpload } from "react-icons/md";
|
||||
import StorageContext from "../../../contexts/StorageContext";
|
||||
import { handleKeyDown } from "../../../utils";
|
||||
import Heading from "../../shared/Heading";
|
||||
import Input from "../../shared/Input";
|
||||
import styles from "./Profile.module.css";
|
||||
|
||||
const Profile = () => {
|
||||
const fileInputRef = useRef(null);
|
||||
const { uploadPhotograph } = useContext(StorageContext);
|
||||
|
||||
const handleIconClick = () => {
|
||||
fileInputRef.current.click();
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
uploadPhotograph(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>Profile</Heading>
|
||||
<div className="flex items-center">
|
||||
<div className={styles.circle}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className={styles.circle}
|
||||
onClick={handleIconClick}
|
||||
onKeyDown={(e) => handleKeyDown(e, handleIconClick)}
|
||||
>
|
||||
<MdFileUpload size="22px" />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input label="Photograph" className="ml-6" path="profile.photograph" />
|
||||
<Input label="Photograph" className="pl-6" path="profile.photograph" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex: 0 0 60px;
|
||||
@apply flex items-center justify-center bg-secondary text-secondary-dark rounded-full;
|
||||
@apply flex items-center justify-center cursor-pointer bg-secondary text-secondary-dark rounded-full;
|
||||
}
|
||||
|
||||
40
src/components/builder/sections/Social.js
Normal file
40
src/components/builder/sections/Social.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { get } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import ModalContext from "../../../contexts/ModalContext";
|
||||
import Heading from "../../shared/Heading";
|
||||
import List from "../lists/List";
|
||||
import SmallListItem from "../lists/small/SmallListItem";
|
||||
|
||||
const Social = ({ state }) => {
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
const path = "social.items";
|
||||
const items = get(state, path, []);
|
||||
|
||||
const handleAdd = () => emitter.emit(events.SOCIAL_MODAL);
|
||||
|
||||
const handleEdit = (data) => emitter.emit(events.SOCIAL_MODAL, data);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>Social Network</Heading>
|
||||
|
||||
<List items={items} onAdd={handleAdd}>
|
||||
{items.map((x, i) => (
|
||||
<SmallListItem
|
||||
key={x.id}
|
||||
data={x}
|
||||
path={path}
|
||||
title={x.network}
|
||||
isFirst={i === 0}
|
||||
subtitle={x.username}
|
||||
onEdit={() => handleEdit(x)}
|
||||
isLast={i === items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Social;
|
||||
@ -1,12 +0,0 @@
|
||||
import React from "react";
|
||||
import Heading from "../../shared/Heading";
|
||||
|
||||
const SocialNetwork = () => {
|
||||
return (
|
||||
<section>
|
||||
<Heading>Social Network</Heading>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialNetwork;
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import ModalContext from "../../contexts/ModalContext";
|
||||
import { handleKeyDown } from "../../utils";
|
||||
import styles from "./CreateResume.module.css";
|
||||
|
||||
const CreateResume = () => {
|
||||
@ -18,12 +19,12 @@ const CreateResume = () => {
|
||||
role="button"
|
||||
className={styles.page}
|
||||
onClick={handleClick}
|
||||
onKeyDown={() => {}}
|
||||
onKeyDown={(e) => handleKeyDown(e, handleClick)}
|
||||
>
|
||||
<MdAdd size="48" />
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
<p>Create New Resume</p>
|
||||
<p>Create Resume</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -64,7 +64,7 @@ const ResumePreview = ({ resume }) => {
|
||||
>
|
||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<span className="text-red-600">Delete</span>
|
||||
<span className="text-red-600 font-medium">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,7 @@ const PrivateRoute = ({ component: Component, location, ...props }) => {
|
||||
const { user, loading } = useContext(UserContext);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen message="Authenticating..." />;
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import cx from "classnames";
|
||||
import React, { useContext } from "react";
|
||||
import { toUrl } from "gatsby-source-gravatar";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import UserContext from "../../contexts/UserContext";
|
||||
import styles from "./Avatar.module.css";
|
||||
|
||||
const Avatar = ({ className }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const photoURL = useMemo(() => toUrl(user.email, "size=128"), [user.email]);
|
||||
|
||||
return (
|
||||
<img
|
||||
className={cx(styles.container, className)}
|
||||
src={user.photoURL}
|
||||
src={photoURL}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { handleKeyDown } from "../../utils";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
const Button = ({
|
||||
@ -16,13 +17,11 @@ const Button = ({
|
||||
[styles.outline]: outline,
|
||||
});
|
||||
|
||||
const handleKeyDown = () => {};
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={classes}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={(e) => handleKeyDown(e, onClick)}
|
||||
onClick={isLoading ? undefined : onClick}
|
||||
>
|
||||
{icon && <Icon size="14" className="mr-2" />}
|
||||
|
||||
@ -11,6 +11,9 @@ const Input = ({
|
||||
label,
|
||||
value,
|
||||
error,
|
||||
onBlur,
|
||||
touched,
|
||||
checked,
|
||||
onChange,
|
||||
className,
|
||||
placeholder,
|
||||
@ -41,11 +44,13 @@ const Input = ({
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
{...(path && inputProps(path))}
|
||||
/>
|
||||
<p>{error}</p>
|
||||
{touched && <p>{error}</p>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 }}>
|
||||
|
||||
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 = {
|
||||
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}`);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@ -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 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
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 { 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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 />
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user