diff --git a/package.json b/package.json index 14d05f6..b61c0d4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,10 @@ "private": true, "dependencies": { "draft-js": "^0.11.0", + "firebase": "^6.6.0", "react": "^16.9.0", "react-dom": "^16.9.0", + "react-firebaseui": "^4.0.0", "react-ga": "^2.6.0", "react-icons": "^3.7.0", "react-scripts": "3.1.1" diff --git a/src/App.tsx b/src/App.tsx index af90fa5..01cd697 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { Header } from './components/Header' import { NoteEditor } from './components/NoteEditor' import { WorkspaceContextProvider } from './context/WorkspaceContext' import { DarkModeContextProvider } from './context/DarkModeContext' +import { AuthContextProvider } from './context/AuthContext' import { initAnalytics, trackPage } from './utils/tracking.js' export const App = () => { @@ -15,13 +16,15 @@ export const App = () => { }, []) return ( - - -
-
- -
-
-
+ + + +
+
+ +
+
+
+
) } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bccb656..81cfd23 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,6 +2,7 @@ import React from 'react' import { IoIosMoon } from 'react-icons/io' import { useDarkModeContext } from '../context/DarkModeContext' import { WorkspaceSelector } from '../components/WorkspaceSelector' +import { UserAccount } from './UserAccount' export const Header = () => { const { darkMode, toggleDarkMode } = useDarkModeContext() @@ -9,10 +10,13 @@ export const Header = () => { return (
- toggleDarkMode()} - /> +
+ toggleDarkMode()} + /> + +
) } diff --git a/src/components/UserAccount.tsx b/src/components/UserAccount.tsx new file mode 100644 index 0000000..e8727cc --- /dev/null +++ b/src/components/UserAccount.tsx @@ -0,0 +1,133 @@ +import React, { useState } from 'react' +import { IoIosLogIn, IoIosLogOut, IoIosSettings } from 'react-icons/io' +import { GiEuropeanFlag } from 'react-icons/gi' +import { GoCheck, GoX } from 'react-icons/go' +import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth' +import { useAuthContext } from '../context/AuthContext' +import { uiConfig, firebase, logout } from '../utils/firebase' + +const LogoutAction = ({ darkMode }) => { + const [confirmLogout, setConfirmLogout] = useState(false) + + const logoutOnClick = () => { + logout() + } + + const toggleConfirmLogout = () => setConfirmLogout(!confirmLogout) + + return ( +
+

Logout

+
+ {confirmLogout ? ( +
+ + toggleConfirmLogout()} + /> +
+ ) : ( + toggleConfirmLogout()} + /> + )} +
+
+ ) +} + +const UserAccountOptions = ({ darkMode }) => { + const [showOptions, setShowOptions] = useState(false) + + const onClick = () => setShowOptions(!showOptions) + + return ( +
+ + {showOptions && ( +
+
+ +
+
+ )} +
+ ) +} + +const LoginAction = ({ darkMode }) => { + const [showOptions, setShowOptions] = useState(false) + + const onClick = () => setShowOptions(!showOptions) + + return ( +
+ + {showOptions && ( +
+
+ +
+
+ )} +
+ ) +} + +export const UserAccount = ({ darkMode }) => { + const { authenticating, isLoggedIn } = useAuthContext() + + return ( + <> + {authenticating ? ( +
+ +
+ ) : ( +
+ {isLoggedIn ? ( + + ) : ( + + )} +
+ )} + + ) +} diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js new file mode 100644 index 0000000..311f1f9 --- /dev/null +++ b/src/context/AuthContext.js @@ -0,0 +1,30 @@ +import React, { createContext, useContext, useState, useEffect } from 'react' +import { firebase } from '../utils/firebase' + +const AuthContext = createContext(null) + +export const AuthContextProvider = ({ children }) => { + const [phone, setPhone] = useState('') + const [authenticating, setAuthenticating] = useState(true) + const [isLoggedIn, setIsLoggedIn] = useState(false) + + useEffect(() => { + const unregister = firebase.auth().onAuthStateChanged(user => { + // Used only on App startup while Firebase checks for persisted Auth state + setAuthenticating(false) + setIsLoggedIn(!!user) + setPhone(!!user ? user.phoneNumber : '') + }) + return () => { + unregister() + } + }, []) + + return ( + + {children} + + ) +} + +export const useAuthContext = () => useContext(AuthContext) diff --git a/src/css/App.scss b/src/css/App.scss index 7747c06..b4dcd2a 100644 --- a/src/css/App.scss +++ b/src/css/App.scss @@ -64,6 +64,20 @@ p.text-input-error { display: none; } +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.loading-icon { + @include loading-icon(4px); + animation: spin 2.4s linear infinite; +} + /* Header ======================================= */ $header-height: 72px; @@ -73,7 +87,7 @@ $header-height: 72px; position: relative; width: 100%; height: $header-height; - padding: 0 20%; + padding: 0 15%; background-color: $white; box-shadow: $generic-box-shadow; @@ -86,17 +100,90 @@ $header-height: 72px; box-shadow: $generic-box-shadow-dark; } - .dark-mode-toggle { + &__actions { + @include va(); position: absolute; - right: 20%; - @include generic-icon(); + right: 15%; @include mq(900px) { right: 16px; } - &.dark-mode { - @include dark-mode-styles(true); + .dark-mode-toggle { + @include generic-icon(4px); + + &.dark-mode { + @include dark-mode-styles(true); + } + } + + /* User Account + ======================================= */ + .user-account { + &__login { + @include va(center); + position: relative; + + &-options { + position: absolute; + width: 200px; + max-height: 450px; + overflow-y: scroll; + top: calc(100% + 14px); + right: 0; + background-color: $white; + box-shadow: rgba(#000000, 0.24) 0px 4px 4px 0px; + border-top: $generic-border; + z-index: 200; + + &.auto-width { + width: 352px; + + @include mq(900px) { + width: 100vw; + } + } + + @include mq(900px) { + width: 100vw; + right: -16px; + } + + &.dark-mode { + @include dark-mode-styles(); + border-color: rgba(#000000, 0.24); + } + + &-item { + padding: 8px 16px; + + &.zero-padding { + padding: 0; + } + + &.dark-mode { + border-color: rgba(#000000, 0.24); + } + } + } + + &-toggle { + @include generic-icon(4px); + + &.dark-mode { + @include dark-mode-styles(true); + } + + &.active { + color: $white; + background-color: darken($green, 16); + + &:hover { + background-color: $green; + } + } + } + } } } } @@ -165,7 +252,7 @@ $header-height: 72px; &__list { position: absolute; width: 100%; - max-height: 300px; + max-height: 450px; overflow-y: scroll; top: 100%; left: 50%; @@ -209,10 +296,10 @@ $header-height: 72px; &.delete-action { color: $white; - background-color: $red; + background-color: darken($red, 16); &:hover { - background-color: darken($red, 16); + background-color: $red; } } } diff --git a/src/css/Palette.scss b/src/css/Palette.scss index 28455cf..d559fee 100644 --- a/src/css/Palette.scss +++ b/src/css/Palette.scss @@ -9,3 +9,4 @@ $white: #ffffff; /* Colors ======================================= */ $red: #d96668; +$green: #65da65; diff --git a/src/css/Vars-Mixins.scss b/src/css/Vars-Mixins.scss index 2dd1a23..70b9414 100644 --- a/src/css/Vars-Mixins.scss +++ b/src/css/Vars-Mixins.scss @@ -35,7 +35,8 @@ $generic-border-dark: 1px solid $light-gray; } } -@mixin generic-icon() { +@mixin generic-icon($margin: 0) { + margin: $margin; padding: 4px; border: 0; border-radius: 4px; @@ -47,6 +48,13 @@ $generic-border-dark: 1px solid $light-gray; } } +@mixin loading-icon($margin: 0) { + margin: $margin; + padding: 4px; + border: 0; + font-size: 2.5em; +} + @mixin dark-mode-styles($hover-styles: false) { color: $white; background-color: $black; diff --git a/src/utils/firebase.js b/src/utils/firebase.js new file mode 100644 index 0000000..103b5fb --- /dev/null +++ b/src/utils/firebase.js @@ -0,0 +1,38 @@ +import app from 'firebase/app' +import 'firebase/auth' +import 'firebase/firestore' + +const firebaseConfig = { + apiKey: process.env.REACT_APP_FIREBASE_API_KEY, + authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, + databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL, + projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, + storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.REACT_APP_FIREBASE_APP_ID, +} + +app.initializeApp(firebaseConfig) + +export const uiConfig = { + signInFlow: 'redirect', + signInOptions: [ + { + provider: app.auth.PhoneAuthProvider.PROVIDER_ID, + }, + ], + callbacks: { + signInSuccessWithAuthResult: () => true, + }, +} + +export const logout = () => { + try { + app.auth().signOut() + } catch (error) { + // eslint-disable-next-line + console.error({ error }) + } +} + +export { app as firebase }