mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
- integrating sign in with google
- set up dark mode context and other services
This commit is contained in:
@ -1,5 +1,18 @@
|
||||
import "./src/styles/global.css";
|
||||
|
||||
import React from "react";
|
||||
import "firebase/auth";
|
||||
import "firebase/analytics";
|
||||
import "firebase/firestore";
|
||||
import { ThemeProvider } from "./src/contexts/ThemeContext";
|
||||
|
||||
import "./src/styles/tailwind.css";
|
||||
import "./src/styles/global.css";
|
||||
import { ModalProvider } from "./src/contexts/ModalContext";
|
||||
import { UserProvider } from "./src/contexts/UserContext";
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<ThemeProvider>
|
||||
<ModalProvider>
|
||||
<UserProvider>{element}</UserProvider>
|
||||
</ModalProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -15207,6 +15207,16 @@
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
||||
"integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg=="
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-6.0.8.tgz",
|
||||
"integrity": "sha512-NSqCNwv+C4IfR+c92PFZiNyeBwOJvigrP2bcRi2f6Hg3WqcHhEHOknbSQOs9QDFuqUjmK3SOrdvScQ3z63ifXg==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-transition-group": "^4.4.1"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
|
||||
@ -32,7 +32,8 @@
|
||||
"react-firebase-hooks": "^2.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^3.10.0",
|
||||
"react-loader-spinner": "^3.1.14"
|
||||
"react-loader-spinner": "^3.1.14",
|
||||
"react-toastify": "^6.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.0.5",
|
||||
|
||||
66
src/components/landing/Hero.js
Normal file
66
src/components/landing/Hero.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useStaticQuery, graphql } from "gatsby";
|
||||
import GatsbyImage from "gatsby-image";
|
||||
import ThemeContext from "../../contexts/ThemeContext";
|
||||
import ModalContext from "../../contexts/ModalContext";
|
||||
import UserContext from "../../contexts/UserContext";
|
||||
import Button from "../shared/Button";
|
||||
|
||||
const Hero = () => {
|
||||
const { user, loading } = useContext(UserContext);
|
||||
const { toggleDarkMode } = useContext(ThemeContext);
|
||||
const { authModal } = useContext(ModalContext);
|
||||
const { file } = useStaticQuery(graphql`
|
||||
query {
|
||||
file(relativePath: { eq: "logo.png" }) {
|
||||
childImageSharp {
|
||||
fixed(width: 256, height: 256) {
|
||||
...GatsbyImageSharpFixed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const handleLogin = () => authModal.setOpen(true);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<GatsbyImage
|
||||
className="shadow-md rounded"
|
||||
fixed={file.childImageSharp.fixed}
|
||||
/>
|
||||
|
||||
<div className="ml-12">
|
||||
<h1 className="text-5xl font-bold">Reactive Resume</h1>
|
||||
<h2 className="mt-1 text-3xl text-gray-500">
|
||||
A free and open-source resume builder.
|
||||
</h2>
|
||||
|
||||
<div className="mt-12 flex">
|
||||
{user ? (
|
||||
<Button
|
||||
title="Go to App"
|
||||
onClick={handleLogin}
|
||||
isLoading={loading || authModal.isOpen}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
title="Login"
|
||||
onClick={handleLogin}
|
||||
isLoading={loading || authModal.isOpen}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
outline
|
||||
className="ml-8"
|
||||
title="Source Code"
|
||||
onClick={toggleDarkMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
@ -16,7 +16,7 @@ const Button = ({ title, isLoading, onClick, outline, className }) => {
|
||||
<div
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
onClick={isLoading ? undefined : onClick}
|
||||
className={classes}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
|
||||
23
src/components/shared/Wrapper.js
Normal file
23
src/components/shared/Wrapper.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { ToastContainer, Slide } from "react-toastify";
|
||||
import ModalRegistrar from "../../modals/ModalRegistrar";
|
||||
|
||||
const Wrapper = ({ children }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{children}
|
||||
|
||||
<ModalRegistrar />
|
||||
<ToastContainer
|
||||
role="alert"
|
||||
hideProgressBar
|
||||
transition={Slide}
|
||||
closeButton={false}
|
||||
position="bottom-right"
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
25
src/contexts/ModalContext.js
Normal file
25
src/contexts/ModalContext.js
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { createContext, useState } from "react";
|
||||
|
||||
const defaultState = {
|
||||
authModal: {},
|
||||
};
|
||||
|
||||
const ModalContext = createContext(defaultState);
|
||||
|
||||
const ModalProvider = ({ children }) => {
|
||||
const [authOpen, setAuthOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
authModal: { isOpen: authOpen, setOpen: setAuthOpen },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalContext;
|
||||
|
||||
export { ModalProvider };
|
||||
59
src/contexts/ThemeContext.js
Normal file
59
src/contexts/ThemeContext.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useState, useEffect, createContext } from "react";
|
||||
|
||||
const COLOR_CONFIG = {
|
||||
light: {
|
||||
"--color-primary": "#444",
|
||||
"--color-primary-dark": "#333",
|
||||
"--color-inverse": "#fff",
|
||||
"--color-inverse-dark": "#f5f5f5",
|
||||
},
|
||||
dark: {
|
||||
"--color-primary": "#f5f5f5",
|
||||
"--color-primary-dark": "#eee",
|
||||
"--color-inverse": "#212121",
|
||||
"--color-inverse-dark": "#121212",
|
||||
},
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
darkMode: false,
|
||||
toggleDarkMode: () => {},
|
||||
};
|
||||
|
||||
const ThemeContext = createContext(defaultState);
|
||||
|
||||
const ThemeProvider = ({ children }) => {
|
||||
const [darkMode, setDarkMode] = useState(defaultState.darkMode);
|
||||
|
||||
useEffect(() => {
|
||||
const isDarkMode = JSON.parse(localStorage.getItem("darkMode"));
|
||||
isDarkMode ? setDarkMode(true) : setDarkMode(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const colorConfig = darkMode ? COLOR_CONFIG.dark : COLOR_CONFIG.light;
|
||||
for (const [key, value] of Object.entries(colorConfig)) {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
localStorage.setItem("darkMode", JSON.stringify(!darkMode));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
darkMode,
|
||||
toggleDarkMode,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
|
||||
export { ThemeProvider };
|
||||
52
src/contexts/UserContext.js
Normal file
52
src/contexts/UserContext.js
Normal file
@ -0,0 +1,52 @@
|
||||
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";
|
||||
|
||||
const defaultState = {
|
||||
user: null,
|
||||
logout: () => {},
|
||||
loginWithGoogle: async () => {},
|
||||
};
|
||||
|
||||
const UserContext = createContext(defaultState);
|
||||
|
||||
const UserProvider = ({ children }) => {
|
||||
const [firebaseUser, loading] = useAuthState(firebase.auth());
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setUser(firebaseUser);
|
||||
}, [firebaseUser]);
|
||||
|
||||
const loginWithGoogle = async () => {
|
||||
const provider = new firebase.auth.GoogleAuthProvider();
|
||||
|
||||
try {
|
||||
await firebase.auth().signInWithPopup(provider);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
firebase.auth().signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
loading,
|
||||
logout,
|
||||
loginWithGoogle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserContext;
|
||||
|
||||
export { UserProvider };
|
||||
@ -1,10 +1,45 @@
|
||||
import React from "react";
|
||||
import React, { useState, useContext, Fragment } from "react";
|
||||
import BaseModal from "./BaseModal";
|
||||
import Button from "../components/shared/Button";
|
||||
import ModalContext from "../contexts/ModalContext";
|
||||
import UserContext from "../contexts/UserContext";
|
||||
|
||||
const AuthModal = () => {
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const { authModal } = useContext(ModalContext);
|
||||
const { user, loginWithGoogle, logout } = useContext(UserContext);
|
||||
|
||||
const handleSignInWithGoogle = async () => {
|
||||
setLoading(true);
|
||||
await loginWithGoogle();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleGoToApp = () => {
|
||||
console.log("Go to App");
|
||||
};
|
||||
|
||||
const loggedInAction = (
|
||||
<Fragment>
|
||||
<Button className="mr-6" title="Go to App" onClick={handleGoToApp} />
|
||||
<Button title="Logout" onClick={logout} />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const loggedOutAction = (
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
title="Sign in with Google"
|
||||
onClick={handleSignInWithGoogle}
|
||||
/>
|
||||
);
|
||||
|
||||
const AuthModal = ({ state }) => {
|
||||
return (
|
||||
<BaseModal state={state} title="Who are you?" action={action}>
|
||||
<BaseModal
|
||||
state={authModal}
|
||||
title="Who are you?"
|
||||
action={user ? loggedInAction : loggedOutAction}
|
||||
>
|
||||
<p>
|
||||
Reactive Resume needs to know who you are so it can securely
|
||||
authenticate you into the app and show you only your information. Once
|
||||
@ -15,6 +50,4 @@ const AuthModal = ({ state }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const action = <Button title="Sign in with Google" />;
|
||||
|
||||
export default AuthModal;
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import React from "react";
|
||||
import Modal from "@material-ui/core/Modal";
|
||||
import Backdrop from "@material-ui/core/Backdrop";
|
||||
import Fade from "@material-ui/core/Fade";
|
||||
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 [isOpen, setOpen] = state;
|
||||
const { isOpen, setOpen } = state;
|
||||
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={handleClose} className={styles.root}>
|
||||
<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>
|
||||
@ -30,6 +39,7 @@ const BaseModal = ({ title, state, children, action }) => {
|
||||
{action}
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
12
src/modals/ModalRegistrar.js
Normal file
12
src/modals/ModalRegistrar.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React, { Fragment } from "react";
|
||||
import AuthModal from "./AuthModal";
|
||||
|
||||
const ModalRegistrar = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<AuthModal />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalRegistrar;
|
||||
12
src/pages/404.js
Normal file
12
src/pages/404.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { navigate } from "gatsby";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const NotFoundPage = () => {
|
||||
useEffect(() => {
|
||||
navigate("/");
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@ -1,61 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import { graphql } from "gatsby";
|
||||
import GatsbyImage from "gatsby-image";
|
||||
import Button from "../components/shared/Button";
|
||||
import AuthModal from "../modals/AuthModal";
|
||||
|
||||
const Home = ({ data }) => {
|
||||
const [isAuthModalOpen, setAuthModalOpen] = useState(false);
|
||||
|
||||
const handleLogin = () => setAuthModalOpen(true);
|
||||
import React from "react";
|
||||
import Wrapper from "../components/shared/Wrapper";
|
||||
import Hero from "../components/landing/Hero";
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="container mt-24">
|
||||
<div className="flex items-center">
|
||||
<GatsbyImage
|
||||
className="shadow-md rounded"
|
||||
fixed={data.file.childImageSharp.fixed}
|
||||
/>
|
||||
|
||||
<div className="ml-12">
|
||||
<h1 className="text-5xl font-bold">Reactive Resume</h1>
|
||||
<h2 className="mt-1 text-3xl text-gray-500">
|
||||
A free and open-source resume builder.
|
||||
</h2>
|
||||
|
||||
<div className="mt-12 flex">
|
||||
<Button title="Login" onClick={handleLogin} />
|
||||
<AuthModal state={[isAuthModalOpen, setAuthModalOpen]} />
|
||||
<Button className="ml-8" outline title="Source Code" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Hero />
|
||||
|
||||
<div className="pt-8">
|
||||
<Feature title="Create a resume that’s worthy of who you are.">
|
||||
Keep up with the latest trends in resume design without having to
|
||||
start from scratch. With new templates being designed every week and
|
||||
having made it that easy to design your own templates and submit them
|
||||
to the community, you’ll never have to copy and edit your friend’s
|
||||
resume again.
|
||||
having made it that easy to design your own templates and submit
|
||||
them to the community, you’ll never have to copy and edit your
|
||||
friend’s resume again.
|
||||
</Feature>
|
||||
|
||||
<Feature title="Updating your resume shouldn’t be a chore.">
|
||||
The biggest problem I’ve faced was when I had to update my resume when
|
||||
I learned a new skill or found a new job. The ever-shifting layouts
|
||||
and inconsistency with design over a number of years made it difficult
|
||||
to update your own resume, but Reactive Resume makes it as easy as few
|
||||
clicks.
|
||||
The biggest problem I’ve faced was when I had to update my resume
|
||||
when I learned a new skill or found a new job. The ever-shifting
|
||||
layouts and inconsistency with design over a number of years made it
|
||||
difficult to update your own resume, but Reactive Resume makes it as
|
||||
easy as few clicks.
|
||||
</Feature>
|
||||
|
||||
<Feature title="Kickstarting your career shouldn’t come at a cost.">
|
||||
There are brilliant alternatives to this app like{" "}
|
||||
<a href="/">Novoresume</a> and <a href="/">Zety</a>, but they come at
|
||||
a cost, mainly because of the time the developers and the marketing
|
||||
they had to incur to make the product. This app might not be better
|
||||
than them, but it does cater to people who are just not in a position
|
||||
to pay hundreds of dollars to create a resume to bootstrap their
|
||||
career.
|
||||
<a href="/">Novoresume</a> and <a href="/">Zety</a>, but they come
|
||||
at a cost, mainly because of the time the developers and the
|
||||
marketing they had to incur to make the product. This app might not
|
||||
be better than them, but it does cater to people who are just not in
|
||||
a position to pay hundreds of dollars to create a resume to
|
||||
bootstrap their career.
|
||||
</Feature>
|
||||
</div>
|
||||
|
||||
@ -66,6 +43,7 @@ const Home = ({ data }) => {
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -78,16 +56,4 @@ const Feature = ({ title, children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const query = graphql`
|
||||
query {
|
||||
file(relativePath: { eq: "logo.png" }) {
|
||||
childImageSharp {
|
||||
fixed(width: 256, height: 256) {
|
||||
...GatsbyImageSharpFixed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Home;
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
:root {
|
||||
--color-primary: #444;
|
||||
--color-primary-dark: #333;
|
||||
--color-inverse: #fff;
|
||||
--color-inverse-dark: #f5f5f5;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "~react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
@import "~react-toastify/dist/ReactToastify.css";
|
||||
@import "./toastify.css";
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 12px;
|
||||
@apply text-primary;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
@apply text-primary bg-inverse;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
5
src/styles/tailwind.css
Normal file
5
src/styles/tailwind.css
Normal file
@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
12
src/styles/toastify.css
Normal file
12
src/styles/toastify.css
Normal file
@ -0,0 +1,12 @@
|
||||
.Toastify__toast {
|
||||
@apply px-8 py-6 shadow-lg rounded;
|
||||
}
|
||||
|
||||
.Toastify__toast--default {
|
||||
@apply bg-primary text-inverse;
|
||||
}
|
||||
|
||||
.Toastify__toast-body {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
@apply font-medium;
|
||||
}
|
||||
Reference in New Issue
Block a user