- creating and updating resumes
This commit is contained in:
parent
e1f1d84201
commit
e247cb102c
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"files.associations": {
|
||||
"*.js": "javascriptreact"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue