- designing the dashboard

- resume preview
- create resume modal
This commit is contained in:
Amruth Pillai
2020-07-04 10:26:29 +05:30
parent dd5e594dc9
commit e1f1d84201
27 changed files with 556 additions and 26 deletions

View File

@ -0,0 +1,34 @@
import React, { useContext } from "react";
import { MdAdd } from "react-icons/md";
import styles from "./CreateResume.module.css";
import ModalContext from "../../contexts/ModalContext";
const CreateResume = () => {
const { createResumeModal } = useContext(ModalContext);
const handleClick = () => {
createResumeModal.setOpen(true);
};
return (
<div className={styles.resume}>
<div className={styles.backdrop}>
<MdAdd color="#FFF" size="48" />
</div>
<div
tabIndex="0"
role="button"
className={styles.page}
onClick={handleClick}
onKeyDown={() => {}}
>
<MdAdd color="#444" size="48" />
</div>
<div className={styles.meta}>
<p>Create New Resume</p>
</div>
</div>
);
};
export default CreateResume;

View File

@ -0,0 +1,31 @@
.resume {
@apply relative flex flex-col items-center;
}
.resume > .backdrop {
max-width: 184px;
height: 260px;
@apply rounded absolute w-full bg-black shadow;
@apply absolute text-gray-500 flex justify-center items-center;
}
.resume > .page {
max-width: 184px;
height: 260px;
@apply rounded absolute w-full bg-white;
@apply transition-opacity duration-200 ease-in-out;
@apply cursor-pointer absolute text-gray-500 flex justify-center items-center;
}
.resume > .page:hover {
@apply transition-opacity duration-200 ease-in-out opacity-25;
}
.resume > .meta {
margin-top: 260px;
@apply text-center;
}
.resume > .meta p {
@apply mt-3 font-medium leading-normal;
}

View File

@ -0,0 +1,64 @@
import React, { useState } from "react";
import { MdMoreHoriz, MdOpenInNew } from "react-icons/md";
import { Menu, MenuItem } from "@material-ui/core";
import styles from "./ResumePreview.module.css";
const ResumePreview = ({ title, subtitle }) => {
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = () => {
console.log("Hello, World!");
};
const handleMenuClick = (event) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
return (
<div className={styles.resume}>
<div className={styles.backdrop}>
<img
src="https://source.unsplash.com/random/210x297"
alt="Resume Preview"
/>
</div>
<div className={styles.page}>
<MdOpenInNew
color="#fff"
size="48"
className="cursor-pointer"
onClick={handleClick}
/>
<MdMoreHoriz
color="#fff"
size="48"
className="cursor-pointer"
aria-haspopup="true"
onClick={handleMenuClick}
/>
<Menu
keepMounted
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleMenuClose}>Duplicate</MenuItem>
<MenuItem onClick={handleMenuClose}>
<span className="text-red-600">Delete</span>
</MenuItem>
</Menu>
</div>
<div className={styles.meta}>
<p>{title}</p>
<span>{subtitle}</span>
</div>
</div>
);
};
export default ResumePreview;

View File

@ -0,0 +1,40 @@
.resume {
@apply relative flex flex-col items-center;
}
.resume > .backdrop {
max-width: 184px;
height: 260px;
@apply rounded absolute w-full bg-black shadow;
}
.resume > .backdrop img {
max-width: 184px;
height: 260px;
@apply w-full object-cover rounded;
}
.resume > .page {
max-width: 184px;
height: 260px;
@apply rounded absolute w-full bg-black;
@apply opacity-0 transition-opacity duration-200 ease-in-out;
@apply absolute text-gray-500 flex flex-col justify-evenly items-center;
}
.resume > .page:hover {
@apply opacity-75 transition-opacity duration-200 ease-in-out;
}
.resume > .meta {
margin-top: 260px;
@apply flex flex-col items-center;
}
.resume > .meta p {
@apply mt-3 font-medium leading-normal;
}
.resume > .meta span {
font-size: 10px;
}

View File

@ -0,0 +1,41 @@
import React, { useContext } from "react";
import Logo from "../shared/Logo";
import UserContext from "../../contexts/UserContext";
import styles from "./TopNavbar.module.css";
import { navigate, Link } from "gatsby";
const TopNavbar = () => {
const { user, logout } = useContext(UserContext);
const handleLogout = async () => {
await logout();
navigate("/");
};
return (
<div className={styles.navbar}>
<div className="container">
<Link to="/">
<Logo size="40px" />
</Link>
<div className="flex items-center">
<button
className="text-primary font-semibold focus:outline-none hover:underline"
onClick={handleLogout}
>
Logout
</button>
<img
className="ml-8 h-12 rounded-full"
src={user.photoURL}
alt={user.displayName}
/>
</div>
</div>
</div>
);
};
export default TopNavbar;

View File

@ -0,0 +1,8 @@
.navbar {
height: 65px;
@apply w-full shadow;
}
.navbar > div {
@apply h-full flex items-center justify-between;
}

View File

@ -5,6 +5,7 @@ import ModalContext from "../../contexts/ModalContext";
import UserContext from "../../contexts/UserContext";
import Button from "../shared/Button";
import Logo from "../shared/Logo";
import { navigate } from "gatsby";
const Hero = () => {
const { user, loading } = useContext(UserContext);
@ -13,9 +14,11 @@ const Hero = () => {
const handleLogin = () => authModal.setOpen(true);
const handleGotoApp = () => navigate("/app/dashboard");
return (
<div className="flex items-center">
<Logo size="256px" />
<Logo className="shadow-lg" size="256px" />
<div className="ml-12">
<h1 className="text-5xl font-bold">Reactive Resume</h1>
@ -27,7 +30,7 @@ const Hero = () => {
{user ? (
<Button
title="Go to App"
onClick={handleLogin}
onClick={handleGotoApp}
isLoading={loading || authModal.isOpen}
/>
) : (

View File

@ -0,0 +1,19 @@
import React from "react";
import Modal from "@material-ui/core/Modal";
import Loader from "react-loader-spinner";
import Logo from "../shared/Logo";
const LoadingScreen = () => {
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} />
</div>
</div>
</Modal>
);
};
export default LoadingScreen;

View File

@ -0,0 +1,21 @@
import React, { useContext } from "react";
import { navigate } from "gatsby";
import UserContext from "../../contexts/UserContext";
import LoadingScreen from "./LoadingScreen";
const PrivateRoute = ({ component: Component, location, ...props }) => {
const { user, loading } = useContext(UserContext);
if (loading) {
return <LoadingScreen />;
}
if (!user) {
navigate("/");
return null;
}
return <Component {...props} />;
};
export default PrivateRoute;

View File

@ -0,0 +1,32 @@
import React from "react";
import { v4 as uuidv4 } from "uuid";
import styles from "./Input.module.css";
const Input = ({
label,
name,
value,
onChange,
placeholder,
type = "text",
}) => {
const uuid = uuidv4();
return (
<div className={styles.container}>
<label htmlFor={uuid}>
<span>{label}</span>
<input
id={uuid}
name={name}
type={type}
value={value}
onChange={onChange}
placeholder={placeholder}
/>
</label>
</div>
);
};
export default Input;

View File

@ -0,0 +1,15 @@
.container > label {
@apply flex flex-col;
}
.container > label > span {
@apply mb-1 text-gray-600 font-medium text-sm uppercase;
}
.container > label > input {
@apply py-4 px-4 rounded bg-gray-200 border border-gray-200;
}
.container > label > input:focus {
@apply outline-none border-gray-500;
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { useStaticQuery, graphql } from "gatsby";
import GatsbyImage from "gatsby-image";
const Logo = ({ size = "256px" }) => {
const Logo = ({ size = "256px", className }) => {
const { file } = useStaticQuery(graphql`
query {
file(relativePath: { eq: "logo.png" }) {
@ -18,7 +18,7 @@ const Logo = ({ size = "256px" }) => {
return (
<GatsbyImage
loading="eager"
className="shadow-md rounded"
className={`rounded ${className}`}
style={{ width: size, height: size }}
fluid={file.childImageSharp.fluid}
/>

View File

@ -2,17 +2,23 @@ import React, { createContext, useState } from "react";
const defaultState = {
authModal: {},
createResumeModal: {},
};
const ModalContext = createContext(defaultState);
const ModalProvider = ({ children }) => {
const [authOpen, setAuthOpen] = useState(false);
const [createResumeOpen, setCreateResumeOpen] = useState(false);
return (
<ModalContext.Provider
value={{
authModal: { isOpen: authOpen, setOpen: setAuthOpen },
createResumeModal: {
isOpen: createResumeOpen,
setOpen: setCreateResumeOpen,
},
}}
>
{children}

View File

@ -13,7 +13,7 @@ const defaultUser = {
const defaultState = {
user: defaultUser,
logout: () => {},
logout: async () => {},
loginWithGoogle: async () => {},
};
@ -23,10 +23,16 @@ const UserProvider = ({ children }) => {
const [firebaseUser, loading] = useAuthState(firebase.auth());
const [user, setUser] = useState(null);
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user"));
setUser(user);
}, []);
useEffect(() => {
if (firebaseUser) {
const user = pick(firebaseUser, Object.keys(defaultUser));
setUser(user);
localStorage.setItem("user", JSON.stringify(user));
const addUserToDatabase = async () => {
const docRef = firebase.firestore().collection("users").doc(user.uid);
@ -48,8 +54,9 @@ const UserProvider = ({ children }) => {
}
};
const logout = () => {
firebase.auth().signOut();
const logout = async () => {
await firebase.auth().signOut();
localStorage.removeItem("user");
setUser(null);
};
@ -57,8 +64,8 @@ const UserProvider = ({ children }) => {
<UserContext.Provider
value={{
user,
loading,
logout,
loading,
loginWithGoogle,
}}
>

View File

@ -3,6 +3,7 @@ import BaseModal from "./BaseModal";
import Button from "../components/shared/Button";
import ModalContext from "../contexts/ModalContext";
import UserContext from "../contexts/UserContext";
import { navigate } from "gatsby";
const AuthModal = () => {
const [isLoading, setLoading] = useState(false);
@ -15,8 +16,9 @@ const AuthModal = () => {
setLoading(false);
};
const handleGoToApp = () => {
console.log("Go to App");
const handleGotoApp = () => {
navigate("/app/dashboard");
authModal.setOpen(false);
};
const getTitle = () =>
@ -30,7 +32,7 @@ const AuthModal = () => {
const loggedInAction = (
<Fragment>
<Button outline className="mr-8" title="Logout" onClick={logout} />
<Button title="Go to App" onClick={handleGoToApp} />
<Button title="Go to App" onClick={handleGotoApp} />
</Fragment>
);

View File

@ -1,4 +1,5 @@
import React from "react";
import { isFunction } from "lodash";
import Modal from "@material-ui/core/Modal";
import Backdrop from "@material-ui/core/Backdrop";
import Fade from "@material-ui/core/Fade";
@ -6,10 +7,13 @@ import { MdClose } from "react-icons/md";
import styles from "./BaseModal.module.css";
import Button from "../components/shared/Button";
const BaseModal = ({ title, state, children, action }) => {
const BaseModal = ({ title, state, children, action, onDestroy }) => {
const { isOpen, setOpen } = state;
const handleClose = () => setOpen(false);
const handleClose = () => {
setOpen(false);
isFunction(onDestroy) && onDestroy();
};
return (
<Modal

View File

@ -0,0 +1,55 @@
import React, { useContext } from "react";
import { useFormik } from "formik";
import BaseModal from "./BaseModal";
import ModalContext from "../contexts/ModalContext";
import Button from "../components/shared/Button";
import Input from "../components/shared/Input";
const CreateResumeModal = () => {
const { createResumeModal } = useContext(ModalContext);
const formik = useFormik({
initialValues: {
name: "",
},
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
},
});
const submitAction = (
<Button title="Create Resume" onClick={() => formik.handleSubmit()} />
);
const onDestroy = () => {
formik.resetForm();
};
return (
<BaseModal
state={createResumeModal}
title="Create New 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}
/>
</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;

View File

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

View File

@ -1,7 +1,7 @@
import { navigate } from "gatsby";
import { useEffect } from "react";
const NotFoundPage = () => {
const NotFound = () => {
useEffect(() => {
navigate("/");
}, []);
@ -9,4 +9,4 @@ const NotFoundPage = () => {
return null;
};
export default NotFoundPage;
export default NotFound;

17
src/pages/app.js Normal file
View File

@ -0,0 +1,17 @@
import React from "react";
import { Router, Redirect } from "@reach/router";
import Wrapper from "../components/shared/Wrapper";
import Dashboard from "./app/dashboard";
import PrivateRoute from "../components/router/PrivateRoute";
import NotFound from "./404";
const App = () => (
<Wrapper>
<Router>
<Redirect noThrow from="/app" to="/app/dashboard" exact />
<PrivateRoute path="/app/dashboard" component={Dashboard} />
<NotFound default />
</Router>
</Wrapper>
);
export default App;

View File

@ -0,0 +1,25 @@
import React 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";
const Dashboard = () => {
return (
<Wrapper>
<TopNavbar />
<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"
/>
</div>
</div>
</Wrapper>
);
};
export default Dashboard;

View File

@ -1,5 +1,4 @@
@import "~react-loader-spinner/dist/loader/css/react-spinner-loader.css";
@import "~react-toastify/dist/ReactToastify.css";
@import "./toastify.css";
:root {

View File

@ -1,5 +1,7 @@
@import "~react-toastify/dist/ReactToastify.css";
.Toastify__toast {
@apply px-8 py-6 shadow-lg rounded;
@apply px-8 py-6 shadow rounded;
}
.Toastify__toast--default {