mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
- switching from firestore to realtime DB
- implement debouncing tactic to sync data - display sync indicator
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import { createMuiTheme, MuiThemeProvider } from "@material-ui/core";
|
||||
import "firebase/analytics";
|
||||
import "firebase/auth";
|
||||
import "firebase/firestore";
|
||||
import "firebase/database";
|
||||
import React from "react";
|
||||
import { DashboardProvider } from "./src/contexts/DashboardContext";
|
||||
import { DatabaseProvider } from "./src/contexts/DatabaseContext";
|
||||
import { ModalProvider } from "./src/contexts/ModalContext";
|
||||
import { ResumeProvider } from "./src/contexts/ResumeContext";
|
||||
import { TemplateProvider } from "./src/contexts/TemplateContext";
|
||||
@ -27,11 +27,11 @@ export const wrapRootElement = ({ element }) => (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<ModalProvider>
|
||||
<UserProvider>
|
||||
<DashboardProvider>
|
||||
<DatabaseProvider>
|
||||
<ResumeProvider>
|
||||
<TemplateProvider>{element}</TemplateProvider>
|
||||
</ResumeProvider>
|
||||
</DashboardProvider>
|
||||
</DatabaseProvider>
|
||||
</UserProvider>
|
||||
</ModalProvider>
|
||||
</MuiThemeProvider>
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import React from "react";
|
||||
import { MdPerson } from "react-icons/md";
|
||||
import cx from "classnames";
|
||||
import React, { useContext } from "react";
|
||||
import { MdPerson, MdSync } from "react-icons/md";
|
||||
import DatabaseContext from "../../../contexts/DatabaseContext";
|
||||
import styles from "./RightNavbar.module.css";
|
||||
|
||||
const RightNavbar = () => {
|
||||
const { isUpdating } = useContext(DatabaseContext);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
@ -11,6 +15,8 @@ const RightNavbar = () => {
|
||||
size="20px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MdSync size="24px" className={cx("mt-auto", { spin: isUpdating })} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,14 +4,14 @@ import moment from "moment";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { MdMoreHoriz, MdOpenInNew } from "react-icons/md";
|
||||
import { toast } from "react-toastify";
|
||||
import DashboardContext from "../../contexts/DashboardContext";
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
import ModalContext from "../../contexts/ModalContext";
|
||||
import styles from "./ResumePreview.module.css";
|
||||
|
||||
const ResumePreview = ({ resume }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const { createResumeModal } = useContext(ModalContext);
|
||||
const { deleteResume } = useContext(DashboardContext);
|
||||
const { deleteResume } = useContext(DatabaseContext);
|
||||
|
||||
const handleOpen = () => navigate(`/app/builder/${resume.id}`);
|
||||
|
||||
@ -26,7 +26,7 @@ const ResumePreview = ({ resume }) => {
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteResume(resume);
|
||||
deleteResume(resume.id);
|
||||
toast(`${resume.name} was deleted successfully`);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
@ -70,11 +70,9 @@ const ResumePreview = ({ resume }) => {
|
||||
</Menu>
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
<p>{resume.name}</p>
|
||||
<span>{resume.name}</span>
|
||||
{resume.updatedAt && (
|
||||
<span>
|
||||
Last updated {moment(resume.updatedAt.toDate()).fromNow()}
|
||||
</span>
|
||||
<span>Last updated {moment(resume.updatedAtR).fromNow()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,10 +31,10 @@
|
||||
@apply flex flex-col text-center items-center;
|
||||
}
|
||||
|
||||
.resume > .meta p {
|
||||
.resume > .meta span:first-child {
|
||||
@apply mt-3 font-medium leading-normal;
|
||||
}
|
||||
|
||||
.resume > .meta span {
|
||||
.resume > .meta span:last-child {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import { Fade, Modal } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import Modal from "@material-ui/core/Modal";
|
||||
import Loader from "react-loader-spinner";
|
||||
import Logo from "../shared/Logo";
|
||||
|
||||
const LoadingScreen = () => {
|
||||
const LoadingScreen = ({ type }) => {
|
||||
return (
|
||||
<Modal open hideBackdrop>
|
||||
<div className="w-screen h-screen flex justify-center items-center outline-none">
|
||||
<div className="flex flex-col items-center">
|
||||
<Logo size="48px" className="mb-4" />
|
||||
<Loader type="ThreeDots" color="#AAA" height={32} width={48} />
|
||||
<Fade in>
|
||||
<div className="w-screen h-screen flex justify-center items-center outline-none">
|
||||
<div className="flex flex-col items-center">
|
||||
<Logo size="48px" className="mb-4" />
|
||||
<span className="font-medium opacity-75">Fetching {type}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useContext } from "react";
|
||||
import { navigate } from "gatsby";
|
||||
import React, { useContext } from "react";
|
||||
import UserContext from "../../contexts/UserContext";
|
||||
import LoadingScreen from "./LoadingScreen";
|
||||
|
||||
@ -7,7 +7,7 @@ const PrivateRoute = ({ component: Component, location, ...props }) => {
|
||||
const { user, loading } = useContext(UserContext);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
return <LoadingScreen type="User" />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
@ -15,7 +15,7 @@ const PrivateRoute = ({ component: Component, location, ...props }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
return <Component user={user} {...props} />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
|
||||
@ -20,7 +20,7 @@ const Input = ({
|
||||
const { state, dispatch } = useContext(ResumeContext);
|
||||
|
||||
const inputProps = (path) => ({
|
||||
value: get(state, path),
|
||||
value: get(state, path) || "",
|
||||
onChange: (e) => {
|
||||
dispatch({
|
||||
type: "on_input",
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
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 () => {},
|
||||
deleteResume: async () => {},
|
||||
};
|
||||
|
||||
const DashboardContext = createContext(defaultState);
|
||||
|
||||
const DashboardProvider = ({ 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(),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteResume = async (resume) => {
|
||||
const { id } = resume;
|
||||
await firebase.firestore().collection(collectionRef).doc(id).delete();
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
resumes,
|
||||
createResume,
|
||||
updateResume,
|
||||
deleteResume,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardContext;
|
||||
|
||||
export { DashboardProvider };
|
||||
83
src/contexts/DatabaseContext.js
Normal file
83
src/contexts/DatabaseContext.js
Normal file
@ -0,0 +1,83 @@
|
||||
import firebase from "gatsby-plugin-firebase";
|
||||
import { debounce } from "lodash";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import UserContext from "./UserContext";
|
||||
|
||||
const defaultState = {
|
||||
isUpdating: false,
|
||||
getResume: async () => {},
|
||||
createResume: () => {},
|
||||
updateResume: async () => {},
|
||||
deleteResume: () => {},
|
||||
};
|
||||
|
||||
const DatabaseContext = createContext(defaultState);
|
||||
|
||||
const DatabaseProvider = ({ children }) => {
|
||||
const [isUpdating, setUpdating] = useState(false);
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const getResume = async (id) => {
|
||||
const snapshot = await firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${id}`)
|
||||
.once("value");
|
||||
return snapshot.val();
|
||||
};
|
||||
|
||||
const createResume = (resume) => {
|
||||
const id = uuidv4();
|
||||
const createdAt = firebase.database.ServerValue.TIMESTAMP;
|
||||
|
||||
firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${id}`)
|
||||
.set({
|
||||
id,
|
||||
...resume,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
});
|
||||
};
|
||||
|
||||
const updateResume = async (resume) => {
|
||||
const { id } = resume;
|
||||
|
||||
setUpdating(true);
|
||||
|
||||
await firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${id}`)
|
||||
.update({
|
||||
...resume,
|
||||
updatedAt: firebase.database.ServerValue.TIMESTAMP,
|
||||
});
|
||||
|
||||
setUpdating(false);
|
||||
};
|
||||
|
||||
const debouncedUpdate = debounce(updateResume, 2000);
|
||||
|
||||
const deleteResume = (id) => {
|
||||
firebase.database().ref(`users/${user.uid}/resumes/${id}`).remove();
|
||||
};
|
||||
|
||||
return (
|
||||
<DatabaseContext.Provider
|
||||
value={{
|
||||
isUpdating,
|
||||
getResume,
|
||||
createResume,
|
||||
updateResume: debouncedUpdate,
|
||||
deleteResume,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DatabaseContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseContext;
|
||||
|
||||
export { DatabaseProvider };
|
||||
@ -1,30 +1,26 @@
|
||||
import { set } from "lodash";
|
||||
import React, { createContext, useReducer } from "react";
|
||||
import React, { createContext, useContext, useReducer } from "react";
|
||||
import DatabaseContext from "./DatabaseContext";
|
||||
|
||||
const initialState = {
|
||||
id: "dafa3242-f39a-4755-bab3-be3c3ca3d190",
|
||||
profile: {
|
||||
photograph: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
},
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
};
|
||||
|
||||
const ResumeContext = createContext(initialState);
|
||||
const ResumeContext = createContext({});
|
||||
|
||||
const ResumeProvider = ({ children }) => {
|
||||
const { updateResume } = useContext(DatabaseContext);
|
||||
|
||||
const [state, dispatch] = useReducer((state, { type, payload }) => {
|
||||
let newState;
|
||||
|
||||
switch (type) {
|
||||
case "on_input":
|
||||
return set({ ...state }, payload.path, payload.value);
|
||||
newState = set({ ...state }, payload.path, payload.value);
|
||||
updateResume(newState);
|
||||
return newState;
|
||||
case "set_data":
|
||||
return payload;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}, initialState);
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<ResumeContext.Provider value={{ state, dispatch }}>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { createContext, useState, useEffect } from "react";
|
||||
import { useAuthState } from "react-firebase-hooks/auth";
|
||||
import firebase from "gatsby-plugin-firebase";
|
||||
import { toast } from "react-toastify";
|
||||
import { pick } from "lodash";
|
||||
import React, { createContext, useEffect, useState } from "react";
|
||||
import { useAuthState } from "react-firebase-hooks/auth";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const defaultUser = {
|
||||
uid: null,
|
||||
@ -35,9 +35,9 @@ const UserProvider = ({ children }) => {
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
|
||||
const addUserToDatabase = async () => {
|
||||
const docRef = firebase.firestore().collection("users").doc(user.uid);
|
||||
const snapshot = await docRef.get();
|
||||
!snapshot.exists && docRef.set(user);
|
||||
const userRef = firebase.database().ref(`users/${user.uid}`);
|
||||
const snapshot = await userRef.once("value");
|
||||
!snapshot.val() && userRef.set(user);
|
||||
};
|
||||
|
||||
addUserToDatabase();
|
||||
|
||||
@ -3,7 +3,7 @@ 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 DashboardContext from "../contexts/DashboardContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
import ModalContext from "../contexts/ModalContext";
|
||||
import { getModalText } from "../utils";
|
||||
import BaseModal from "./BaseModal";
|
||||
@ -18,7 +18,7 @@ const CreateResumeModal = ({ data }) => {
|
||||
const modalRef = useRef(null);
|
||||
const [isEditMode, setEditMode] = useState(false);
|
||||
const { createResumeModal } = useContext(ModalContext);
|
||||
const { createResume, updateResume } = useContext(DashboardContext);
|
||||
const { createResume, updateResume } = useContext(DatabaseContext);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
import React from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import Artboard from "../../components/builder/center/Artboard";
|
||||
import LeftSidebar from "../../components/builder/left/LeftSidebar";
|
||||
import RightSidebar from "../../components/builder/right/RightSidebar";
|
||||
import LoadingScreen from "../../components/router/LoadingScreen";
|
||||
import Wrapper from "../../components/shared/Wrapper";
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
import ResumeContext from "../../contexts/ResumeContext";
|
||||
|
||||
const Builder = ({ user, id }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { getResume } = useContext(DatabaseContext);
|
||||
const { dispatch } = useContext(ResumeContext);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const resume = await getResume(id);
|
||||
dispatch({ type: "set_data", payload: resume });
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [id, getResume, dispatch]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen type="Resume" />;
|
||||
}
|
||||
|
||||
const Builder = ({ id }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="h-screen grid grid-cols-11">
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import React, { useContext } from "react";
|
||||
import firebase from "gatsby-plugin-firebase";
|
||||
import React from "react";
|
||||
import { useListVals } from "react-firebase-hooks/database";
|
||||
import CreateResume from "../../components/dashboard/CreateResume";
|
||||
import ResumePreview from "../../components/dashboard/ResumePreview";
|
||||
import TopNavbar from "../../components/dashboard/TopNavbar";
|
||||
import LoadingScreen from "../../components/router/LoadingScreen";
|
||||
import Wrapper from "../../components/shared/Wrapper";
|
||||
import DashboardContext from "../../contexts/DashboardContext";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { resumes } = useContext(DashboardContext);
|
||||
const Dashboard = ({ user }) => {
|
||||
const [resumes, loading] = useListVals(
|
||||
firebase.database().ref(`users/${user.uid}/resumes`)
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@ -16,11 +24,9 @@ const Dashboard = () => {
|
||||
<div className="grid grid-cols-6 gap-8">
|
||||
<CreateResume />
|
||||
|
||||
{resumes
|
||||
.filter((x) => x !== null)
|
||||
.map((x) => (
|
||||
<ResumePreview key={x.id} resume={x} />
|
||||
))}
|
||||
{resumes.map((x) => (
|
||||
<ResumePreview key={x.id} resume={x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
@ -38,3 +38,13 @@ section {
|
||||
#artboard hr {
|
||||
border-color: #eee;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user