mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-15 09:11:57 +10:00
- designing the dashboard
- resume preview - create resume modal
This commit is contained in:
34
src/components/dashboard/CreateResume.js
Normal file
34
src/components/dashboard/CreateResume.js
Normal 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;
|
||||
31
src/components/dashboard/CreateResume.module.css
Normal file
31
src/components/dashboard/CreateResume.module.css
Normal 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;
|
||||
}
|
||||
64
src/components/dashboard/ResumePreview.js
Normal file
64
src/components/dashboard/ResumePreview.js
Normal 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;
|
||||
40
src/components/dashboard/ResumePreview.module.css
Normal file
40
src/components/dashboard/ResumePreview.module.css
Normal 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;
|
||||
}
|
||||
41
src/components/dashboard/TopNavbar.js
Normal file
41
src/components/dashboard/TopNavbar.js
Normal 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;
|
||||
8
src/components/dashboard/TopNavbar.module.css
Normal file
8
src/components/dashboard/TopNavbar.module.css
Normal file
@ -0,0 +1,8 @@
|
||||
.navbar {
|
||||
height: 65px;
|
||||
@apply w-full shadow;
|
||||
}
|
||||
|
||||
.navbar > div {
|
||||
@apply h-full flex items-center justify-between;
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
||||
19
src/components/router/LoadingScreen.js
Normal file
19
src/components/router/LoadingScreen.js
Normal 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;
|
||||
21
src/components/router/PrivateRoute.js
Normal file
21
src/components/router/PrivateRoute.js
Normal 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;
|
||||
32
src/components/shared/Input.js
Normal file
32
src/components/shared/Input.js
Normal 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;
|
||||
15
src/components/shared/Input.module.css
Normal file
15
src/components/shared/Input.module.css
Normal 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;
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
55
src/modals/CreateResumeModal.js
Normal file
55
src/modals/CreateResumeModal.js
Normal 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;
|
||||
@ -1,10 +1,12 @@
|
||||
import React, { Fragment } from "react";
|
||||
import AuthModal from "./AuthModal";
|
||||
import CreateResumeModal from "./CreateResumeModal";
|
||||
|
||||
const ModalRegistrar = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<AuthModal />
|
||||
<CreateResumeModal />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
17
src/pages/app.js
Normal 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;
|
||||
25
src/pages/app/dashboard.js
Normal file
25
src/pages/app/dashboard.js
Normal 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;
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user