From d205ee7841c3d1a304db1bbccac2298a7122f187 Mon Sep 17 00:00:00 2001 From: Patrick Truong Date: Fri, 29 Jan 2021 14:13:34 -0800 Subject: [PATCH 01/19] Implement change password in profile page Fix alert styling when no message present cleanup old prop in Container Add Profile route Add minor utility components; can refactor in future Implement basic profile page & change password functionality --- src/App.js | 4 + src/components/Alert/AlertMessage.js | 12 +- src/components/Layout/Container.js | 7 +- src/components/Layout/Panel/index.js | 5 + src/pages/Profile/SmallLoading.js | 24 ++ src/pages/Profile/index.js | 317 +++++++++++++++++++++++++++ 6 files changed, 358 insertions(+), 11 deletions(-) create mode 100644 src/components/Layout/Panel/index.js create mode 100644 src/pages/Profile/SmallLoading.js create mode 100644 src/pages/Profile/index.js diff --git a/src/App.js b/src/App.js index 9c0a8a2..080825d 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import Recipe from 'pages/Recipe' import Login from 'pages/Login' import CreateAccount from 'pages/CreateAccount' import Activate from 'pages/Activate' +import Profile from 'pages/Profile' import ModalFlowDemo from 'pages/ModalFlowDemo' const Test = () => { @@ -52,6 +53,9 @@ function App() { > + + + diff --git a/src/components/Alert/AlertMessage.js b/src/components/Alert/AlertMessage.js index b38935e..72002e9 100644 --- a/src/components/Alert/AlertMessage.js +++ b/src/components/Alert/AlertMessage.js @@ -150,11 +150,13 @@ const AlertMessage = ({ > {header} -
-

{message}

-
+ {message && ( +
+

{message}

+
+ )} {action && (
{loading ? ( diff --git a/src/components/Layout/Container.js b/src/components/Layout/Container.js index 2c71ae6..371f6ab 100644 --- a/src/components/Layout/Container.js +++ b/src/components/Layout/Container.js @@ -2,12 +2,7 @@ import Header from './Header' import Footer from './Footer' import { Modal } from 'components/Modal' -const Container = ({ - config, - children, - alertDisabled = false, - defaultLayout = true, -}) => { +const Container = ({ config, children, defaultLayout = true }) => { let settings = defaultLayout ? { flexCol: true, diff --git a/src/components/Layout/Panel/index.js b/src/components/Layout/Panel/index.js new file mode 100644 index 0000000..6a681dc --- /dev/null +++ b/src/components/Layout/Panel/index.js @@ -0,0 +1,5 @@ +export const BasicCard = ({ children }) => ( +
+
{children}
+
+) diff --git a/src/pages/Profile/SmallLoading.js b/src/pages/Profile/SmallLoading.js new file mode 100644 index 0000000..8d5a245 --- /dev/null +++ b/src/pages/Profile/SmallLoading.js @@ -0,0 +1,24 @@ +const SmallLoading = () => ( + + + + +) + +export default SmallLoading diff --git a/src/pages/Profile/index.js b/src/pages/Profile/index.js new file mode 100644 index 0000000..8f105b4 --- /dev/null +++ b/src/pages/Profile/index.js @@ -0,0 +1,317 @@ +import axios from 'axios' +import { useState } from 'react' +import { useAuth } from 'context/AuthContext' +import { useAlert, alertType } from 'context/AlertContext' +import { BasicCard } from 'components/Layout/Panel' +import { validatePassword, passwordRequirements } from 'helper/form' +import FormAlert from 'components/FormAlert' +import { AUTH_API } from 'config' +import SmallLoading from './SmallLoading' + +const showAlerts = ({ type, text }) => ( + +) + +function Profile() { + const { barista } = useAuth() + const { addAlert } = useAlert() + const [isEditing, setIsEditing] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [passwordAlerts, setPasswordAlerts] = useState(passwordRequirements) + const [error, setError] = useState({ + message: null, + type: null, + }) + const [state, setState] = useState({ + oldPassword: '', + newPassword: '', + confirmPassword: '', + }) + + const clearError = () => { + setError({ + message: null, + type: null, + }) + } + + const save = async (e) => { + e.preventDefault() + try { + if (state.newPassword === state.confirmPassword) { + // trigger loading animation + setIsLoading(true) + + await axios.put( + AUTH_API + '/change-password', + { + email: barista.email, + currentPassword: state.oldPassword, + newPassword: state.newPassword, + }, + { withCredentials: true } + ) + // turn off loading animation + // set global alert to success + + addAlert({ + type: alertType.SUCCESS, + header: 'Password successfully changed!', + close: true, + }) + setIsLoading(false) + setIsEditing(false) + setState({ oldPassword: '', newPassword: '', confirmPassword: '' }) + } else { + setError({ + message: 'Passwords do not match', + type: 'mismatch', + }) + } + } catch ({ response }) { + setIsLoading(false) + if (!response || response?.status === 500) { + setError({ + message: 'Unable to change password at this time. Try again later.', + type: 'bad_server', + }) + } else if (response?.status === 400) { + setError({ + message: response?.data.message, + type: 'no_change', + }) + } else if (response?.status === 401) { + setError({ + message: 'Incorrect current password', + type: 'bad_password', + }) + } + } + } + + const cancel = () => { + clearError() + setIsEditing(false) + setState({ + oldPassword: '', + newPassword: '', + confirmPassword: '', + }) + } + + const onChange = ({ target }) => { + if ( + error.type === 'bad_server' || + (error.type === 'bad_password' && target.name === 'oldPassword') || + (error.type === 'mismatch' && target.name !== 'oldPassword') || + (error.type === 'no_change' && target.name !== 'confirmPassword') + ) { + clearError() + } + + if (target.name === 'newPassword') { + setPasswordAlerts(validatePassword(target.value)) + } + + setState({ + ...state, + [target.name]: target.value, + }) + } + + const alerts = Object.values(passwordAlerts).filter( + ({ isActive }) => isActive + ) + + const inputStyle = { + default: 'border-gray-300 focus:ring-blue-700 focus:border-blue-700', + error: 'border-red-300 focus:ring-red-700 focus:border-red-700', + } + + const isSaveDisabled = + state.oldPassword === '' || + state.newPassword === '' || + state.confirmPassword === '' || + isLoading || + error.type || + alerts.length > 0 + + return ( + +
+
+

+ Profile +

+

+ Manage your account information here. +

+
+ +
+ +

+ {barista.email} +

+
+ +
+ +

+ {barista.display_name} +

+
+ +
+ + + {isEditing ? ( + <> +
+
+ + +
+
+ + +
+
+ + +
+ {error.type && ( +

{error.message}

+ )} + {alerts.length > 0 && ( +
{alerts.map(showAlerts)}
+ )} +
+
+ + +
+ + ) : ( +
+

+ *************** +

+ +
+ )} +
+
+
+ ) +} +export default Profile From bb07bd7a2ce9d2d8a65c01327a4793c1c7c760b0 Mon Sep 17 00:00:00 2001 From: Patrick Truong Date: Fri, 29 Jan 2021 15:06:38 -0800 Subject: [PATCH 02/19] Add unverified message to profile page Cleanup alert API to allow easier compositions for unverified profile pag Cleanup main nav dropdown links Move alert icons to main icon folder --- src/components/Alert/AlertMessage.js | 195 +----------------- src/components/Alert/Notification.js | 124 +++++++++++ src/components/Icon/Alert.js | 70 +++++++ src/components/Icon/index.js | 3 + src/components/Layout/Header/UserSection.js | 6 +- .../Layout/Header/UserSectionMobile.js | 10 +- src/components/Layout/Header/index.js | 8 +- src/components/Modal/NewUserModal.js | 10 +- src/context/AuthContext.js | 42 ++-- src/helper/auth.js | 29 ++- src/pages/Profile/index.js | 13 +- 11 files changed, 273 insertions(+), 237 deletions(-) create mode 100644 src/components/Alert/Notification.js create mode 100644 src/components/Icon/Alert.js diff --git a/src/components/Alert/AlertMessage.js b/src/components/Alert/AlertMessage.js index 72002e9..c56108f 100644 --- a/src/components/Alert/AlertMessage.js +++ b/src/components/Alert/AlertMessage.js @@ -1,126 +1,7 @@ import { useState } from 'react' -import { alertType } from 'context/AlertContext' -import { Success, Fail, Load } from 'components/Utility/AlertAction' - -const ErrorIcon = () => ( - - - -) -const WarningIcon = () => ( - - - -) -const SuccessIcon = () => ( - - - -) -const InfoIcon = () => ( - - - -) -const CloseIcon = () => ( - - - -) - -const settings = { - [alertType.ERROR]: { - bg: 'bg-red-50', - icon: ErrorIcon, - header: 'text-red-800', - message: 'text-red-600', - action: 'hover:bg-red-100 focus:ring-offset-red-50 focus:ring-red-600', - close: 'text-red-500 hover:bg-red-200 focus:bg-red-200', - }, - [alertType.WARNING]: { - bg: 'bg-yellow-50', - icon: WarningIcon, - header: 'text-yellow-800', - message: 'text-yellow-600', - action: - 'hover:bg-yellow-100 focus:ring-offset-yellow-50 focus:ring-yellow-600', - close: 'text-yellow-500 hover:bg-yellow-200 focus:bg-yellow-200', - }, - [alertType.SUCCESS]: { - bg: 'bg-green-50', - icon: SuccessIcon, - header: 'text-green-800', - message: 'text-green-600', - action: - 'hover:bg-green-100 focus:ring-offset-green-50 focus:ring-green-600', - close: 'text-green-500 hover:bg-green-200 focus:bg-green-200', - }, - [alertType.INFO]: { - bg: 'bg-blue-50', - icon: InfoIcon, - header: 'text-blue-800', - message: 'text-blue-600', - action: 'hover:bg-blue-100 focus:ring-offset-blue-50 focus:ring-blue-600', - close: 'text-blue-500 hover:bg-blue-200 focus:bg-blue-200', - }, -} - -const AlertMessage = ({ - noShadow, - close, - onClose, - type, - header, - message, - action, -}) => { - const Icon = settings[type].icon +import Notification from './Notification' +const AlertMessage = (props) => { const [hasSucceeded, setHasSucceeded] = useState(false) const [hasFailed, setHasFail] = useState(false) const [loading, setLoading] = useState(false) @@ -133,72 +14,16 @@ const AlertMessage = ({ setHasFail(true) } const onLoad = () => setLoading(true) + const actionOnClick = () => props.action?.onClick(onSuccess, onFail, onLoad) return ( -
-
-
- -
-
-

- {header} -

- {message && ( -
-

{message}

-
- )} - {action && ( -
- {loading ? ( - - ) : hasSucceeded ? ( - - ) : hasFailed ? ( - - ) : ( -
- -
- )} -
- )} -
- {close && ( -
-
- -
-
- )} -
-
+ ) } diff --git a/src/components/Alert/Notification.js b/src/components/Alert/Notification.js new file mode 100644 index 0000000..df6faa1 --- /dev/null +++ b/src/components/Alert/Notification.js @@ -0,0 +1,124 @@ +import { Alert } from 'components/Icon' +import { alertType } from 'context/AlertContext' +import { Success, Fail, Load } from 'components/Utility/AlertAction' + +const settings = { + [alertType.ERROR]: { + bg: 'bg-red-50', + icon: Alert.Error, + header: 'text-red-800', + message: 'text-red-600', + action: 'hover:bg-red-100 focus:ring-offset-red-50 focus:ring-red-600', + close: 'text-red-500 hover:bg-red-200 focus:bg-red-200', + }, + [alertType.WARNING]: { + bg: 'bg-yellow-50', + icon: Alert.Warning, + header: 'text-yellow-800', + message: 'text-yellow-600', + action: + 'hover:bg-yellow-100 focus:ring-offset-yellow-50 focus:ring-yellow-600', + close: 'text-yellow-500 hover:bg-yellow-200 focus:bg-yellow-200', + }, + [alertType.SUCCESS]: { + bg: 'bg-green-50', + icon: Alert.Success, + header: 'text-green-800', + message: 'text-green-600', + action: + 'hover:bg-green-100 focus:ring-offset-green-50 focus:ring-green-600', + close: 'text-green-500 hover:bg-green-200 focus:bg-green-200', + }, + [alertType.INFO]: { + bg: 'bg-blue-50', + icon: Alert.Info, + header: 'text-blue-800', + message: 'text-blue-600', + action: 'hover:bg-blue-100 focus:ring-offset-blue-50 focus:ring-blue-600', + close: 'text-blue-500 hover:bg-blue-200 focus:bg-blue-200', + }, +} + +const Notification = ({ + noShadow, + close, + onClose, + type, + header, + message, + action, + actionOnClick, + hasSucceeded, + hasFailed, + loading, +}) => { + const Icon = settings[type].icon + return ( +
+
+
+ +
+
+

+ {header} +

+ {message && ( +
+

{message}

+
+ )} + {action && ( +
+ {loading ? ( + + ) : hasSucceeded ? ( + + ) : hasFailed ? ( + + ) : ( +
+ +
+ )} +
+ )} +
+ {close && ( +
+
+ +
+
+ )} +
+
+ ) +} + +export default Notification diff --git a/src/components/Icon/Alert.js b/src/components/Icon/Alert.js new file mode 100644 index 0000000..b37b2ea --- /dev/null +++ b/src/components/Icon/Alert.js @@ -0,0 +1,70 @@ +export const Error = () => ( + + + +) +export const Warning = () => ( + + + +) +export const Success = () => ( + + + +) +export const Info = () => ( + + + +) +export const Close = () => ( + + + +) diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js index 60de2fd..2bb5108 100644 --- a/src/components/Icon/index.js +++ b/src/components/Icon/index.js @@ -1,3 +1,6 @@ +import * as Alert from './Alert' +export { Alert } + export const Heart = ({ className = 'w-6 h-6' }) => ( { className='w-full text-left px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out' role='menuitem' > - log out + Log out
@@ -112,13 +112,13 @@ const UserSection = ({ links, setDropdownOpen, isDropdownOpen }) => { to='/login' className='mr-3 inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' > - log in + Log in - create account + Create account ) diff --git a/src/components/Layout/Header/UserSectionMobile.js b/src/components/Layout/Header/UserSectionMobile.js index 409d5d2..6dad940 100644 --- a/src/components/Layout/Header/UserSectionMobile.js +++ b/src/components/Layout/Header/UserSectionMobile.js @@ -3,7 +3,7 @@ import { useAuth } from 'context/AuthContext' import PlaceholderAvatar from './PlaceholderAvatar' const UserSectionMobile = ({ links }) => { - const { isAuthenticated, barista } = useAuth() + const { isAuthenticated, barista, logout } = useAuth() return isAuthenticated ? ( <> @@ -45,11 +45,11 @@ const UserSectionMobile = ({ links }) => { ))} @@ -59,13 +59,13 @@ const UserSectionMobile = ({ links }) => { to='/create-account' className='w-full flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 focus:bg-blue-700' > - create account + Create account - log in + Log in ) diff --git a/src/components/Layout/Header/index.js b/src/components/Layout/Header/index.js index c9e134b..e91871d 100644 --- a/src/components/Layout/Header/index.js +++ b/src/components/Layout/Header/index.js @@ -25,12 +25,8 @@ const links = [ const settingLinks = [ { - text: 'profile', - to: '/pour-app', - }, - { - text: 'settings', - to: '/hi/3/name/what', + text: 'Profile', + to: '/profile', }, ] diff --git a/src/components/Modal/NewUserModal.js b/src/components/Modal/NewUserModal.js index 82270c8..940a9d4 100644 --- a/src/components/Modal/NewUserModal.js +++ b/src/components/Modal/NewUserModal.js @@ -64,18 +64,16 @@ export default function NewUserModal() { /> -
+

Account created!

-
-

- Check your email to confirm your new account -

-
+

+ Check your email to confirm your new account +

diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index 5bbcb83..ede245e 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -25,11 +25,12 @@ import { willAuthError, logoutAPI, } from 'helper/auth' -import { AUTH_API, GRAPHQL_API, VERIFY_API } from 'config' +import { AUTH_API, GRAPHQL_API } from 'config' import { GET_BARISTA } from 'queries' import { updates, keys } from 'helper/cache' import { print } from 'graphql' -import { useHistory } from 'react-router-dom' +import { useHistory, useLocation } from 'react-router-dom' +import { createUnverifiedAlert } from 'helper/auth' const AuthContext = createContext() @@ -82,6 +83,7 @@ function AuthProvider({ children }) { const [state, dispatch] = useReducer(reducer, initialState) const history = useHistory() + const location = useLocation() const logout = useCallback(async () => { await logoutAPI() @@ -182,32 +184,8 @@ function AuthProvider({ children }) { const barista = data.data.barista[0] dispatch(['setBarista', barista]) - if (!barista.is_verified) { - addAlert({ - type: alertType.INFO, - header: 'Your account is unverified', - message: - 'Please check your email for a verification link. Check your junk emails as well in case it went there. You may also click the "Resend email" button below if you cannot find your email.', - close: true, - action: { - onClick: async (success, fail, load) => { - try { - load() - await axios.post( - VERIFY_API + '/resend', - { email: barista.email }, - { withCredentials: true } - ) - success() - } catch (e) { - fail() - } - }, - buttonText: 'Resend email', - successMessage: 'Email sent!', - failMessage: 'Sending email failed. Please try again later.', - }, - }) + if (!barista.is_verified && location.pathname !== '/profile') { + addAlert(createUnverifiedAlert(barista.email)) } } @@ -219,7 +197,13 @@ function AuthProvider({ children }) { } } getBarista() - }, [state.isLoggedIn, state.token, state.hasInit, addAlert]) + }, [ + state.isLoggedIn, + state.token, + state.hasInit, + addAlert, + location.pathname, + ]) const login = async (email, password, callback) => { try { diff --git a/src/helper/auth.js b/src/helper/auth.js index 6aba0a4..2abc9ba 100644 --- a/src/helper/auth.js +++ b/src/helper/auth.js @@ -1,5 +1,32 @@ import axios from 'axios' -import { AUTH_API } from 'config' +import { AUTH_API, VERIFY_API } from 'config' +import { alertType } from 'context/AlertContext' + +export const createUnverifiedAlert = (email) => ({ + type: alertType.INFO, + header: 'Your account is unverified', + message: + 'Currently you cannot access all features due to being unverified. Please check your email for a verification link. You may also click the "Resend email" button below if you cannot find your email.', + close: true, + action: { + onClick: async (success, fail, load) => { + try { + load() + await axios.post( + VERIFY_API + '/resend', + { email }, + { withCredentials: true } + ) + success() + } catch (e) { + fail() + } + }, + buttonText: 'Resend email', + successMessage: 'Email sent!', + failMessage: 'Sending email failed. Please try again later.', + }, +}) export const logoutAPI = async () => { // This is to support logging out from all windows diff --git a/src/pages/Profile/index.js b/src/pages/Profile/index.js index 8f105b4..0c0eefb 100644 --- a/src/pages/Profile/index.js +++ b/src/pages/Profile/index.js @@ -3,10 +3,12 @@ import { useState } from 'react' import { useAuth } from 'context/AuthContext' import { useAlert, alertType } from 'context/AlertContext' import { BasicCard } from 'components/Layout/Panel' +import AlertMessage from 'components/Alert/AlertMessage' import { validatePassword, passwordRequirements } from 'helper/form' import FormAlert from 'components/FormAlert' import { AUTH_API } from 'config' import SmallLoading from './SmallLoading' +import { createUnverifiedAlert } from 'helper/auth' const showAlerts = ({ type, text }) => ( @@ -51,14 +53,13 @@ function Profile() { }, { withCredentials: true } ) - // turn off loading animation - // set global alert to success addAlert({ type: alertType.SUCCESS, header: 'Password successfully changed!', close: true, }) + setIsLoading(false) setIsEditing(false) setState({ oldPassword: '', newPassword: '', confirmPassword: '' }) @@ -148,6 +149,14 @@ function Profile() {

+ {!barista.is_verified && ( + + )} +
{/*
Calendar @@ -222,7 +222,7 @@ const BrewLogDetails = ({ brewLogId, brewSelected, setBrewSelected }) => {
- Private Recipe + Private Log
{is_private.toString()} diff --git a/src/components/BrewTrak/CreateBrew.js b/src/components/BrewTrak/CreateBrew.js index c916b08..8abfe5a 100644 --- a/src/components/BrewTrak/CreateBrew.js +++ b/src/components/BrewTrak/CreateBrew.js @@ -123,7 +123,7 @@ const CreateBrew = () => { type='button' className='mt-2 w-full px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150' > - add brew log + Add Brew Log
diff --git a/src/components/BrewTrak/EditBrewForm.js b/src/components/BrewTrak/EditBrewForm.js index 5771ee0..ae3b0f6 100644 --- a/src/components/BrewTrak/EditBrewForm.js +++ b/src/components/BrewTrak/EditBrewForm.js @@ -79,7 +79,7 @@ const EditBrewForm = ({ brewLogs, id }) => { value={state.bean_name_free} onChange={onChangeGenerator('bean_name_free')} placeholder='Enter bean name' - label='bean_name_free' + label='Bean Name' /> { type='button' className='mt-2 w-full px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150' > - edit brew log + Edit Brew Log diff --git a/src/components/Layout/Header/index.js b/src/components/Layout/Header/index.js index e884406..3252750 100644 --- a/src/components/Layout/Header/index.js +++ b/src/components/Layout/Header/index.js @@ -6,19 +6,19 @@ import UserSectionMobile from './UserSectionMobile' const links = [ { - text: 'pour over app', + text: 'Pour Over App', to: '/brewtrak', }, { - text: 'discover brews', + text: 'Discover Brews', to: '/recipe', }, // { - // text: 'buy beans', + // text: 'Buy Beans', // to: '/bean', // }, { - text: 'recipe player', + text: 'Recipe Player', to: '/recipe-player', }, ] diff --git a/src/components/Recipe/EditRecipeForm.js b/src/components/Recipe/EditRecipeForm.js index dc44e2a..1f9b368 100644 --- a/src/components/Recipe/EditRecipeForm.js +++ b/src/components/Recipe/EditRecipeForm.js @@ -137,7 +137,7 @@ const EditRecipeForm = ({ recipe, id }) => { onClick={submitUpdateRecipe} className='mb-4 inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150' > - Edit Review + Edit Recipe ) diff --git a/src/components/Recipe/Review/CreateRecipeReview.js b/src/components/Recipe/Review/CreateRecipeReview.js index 5daa86b..606ce96 100644 --- a/src/components/Recipe/Review/CreateRecipeReview.js +++ b/src/components/Recipe/Review/CreateRecipeReview.js @@ -73,7 +73,6 @@ const CreateRecipeReview = ({ id }) => { > Add Review - )} diff --git a/src/components/Recipe/Review/EditRecipeReviewForm.js b/src/components/Recipe/Review/EditRecipeReviewForm.js index 1378280..c74bac2 100644 --- a/src/components/Recipe/Review/EditRecipeReviewForm.js +++ b/src/components/Recipe/Review/EditRecipeReviewForm.js @@ -61,7 +61,7 @@ const EditRecipeReviewForm = ({ recipeReview, id }) => { onClick={submitUpdateReview} className='mb-4 inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150' > - edit review + Edit Review ) From b2d6bc9e4e44dd16f7ab31101429dc42cd0b472a Mon Sep 17 00:00:00 2001 From: pattruong Date: Fri, 12 Feb 2021 09:27:39 -0800 Subject: [PATCH 13/19] [MINOR] style changes + merge Recipe changes (#144) * Add Placeholder icon * refactor stages table * Add bg gradient * remove comments and add placeholder image * rearrange recipe details layout * add recipe stages to query * remove comment * Readd changes for conditional edit/delete button --- src/components/Icon/index.js | 14 + src/components/Layout/Container.js | 2 +- .../Recipe/CreateRecipe/StageRow.js | 23 +- src/components/Recipe/RecipeDetails.js | 380 +++++++++--------- .../Recipe/Review/CreateRecipeReview.js | 15 +- src/components/Stage/index.js | 24 ++ src/queries/Recipe.js | 14 + 7 files changed, 265 insertions(+), 207 deletions(-) create mode 100644 src/components/Stage/index.js diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js index 8376ee0..de79514 100644 --- a/src/components/Icon/index.js +++ b/src/components/Icon/index.js @@ -1,6 +1,20 @@ import * as Alert from './Alert' export { Alert } +export const PlaceHolder = ({ className = 'h-6 w-6' }) => ( + + + + + +) + export const Plus = ({ className = 'w-6 h-6' }) => ( { return (
diff --git a/src/components/Recipe/CreateRecipe/StageRow.js b/src/components/Recipe/CreateRecipe/StageRow.js index af9da9f..9884f22 100644 --- a/src/components/Recipe/CreateRecipe/StageRow.js +++ b/src/components/Recipe/CreateRecipe/StageRow.js @@ -1,28 +1,9 @@ -import { wordCapitalized } from 'helper/stringHelper' +import { Table } from 'components/Stage' const StageRow = ({ stages, onEdit }) => { return (
- - - - - - - - - - - {stages.map((stage, i) => ( - - - - - - - ))} - -
ActionStart (s)End (s)Weight (g)
{wordCapitalized(stage.action)}{stage.start}{stage.end}{stage.weight}
+ - - Edit Recipe - - - ) : ( - '' - )} +
+

{name}

+

+ Created by {barista?.display_name} on{' '} + +

+
+ {isAuthenticated && user.id === barista?.id && ( +
+ + + Edit Recipe + +
+ )} + -
-
- {/**/} -
-
-
-

- Recipe Details -

-

-
-
-
-
-
- Brew Type -
-
- {brew_type} -
-
-
-
- Bean Weight -
-
- {bean_weight}g -
-
-
-
- Bean Grind -
-
- {bean_grind} -
-
-
-
- Water Amount -
-
- {water_amount}g -
-
-
-
- Water Temp -
-
- {water_temp}F -
-
-
-
- Bean -
-
- {bean_name_free} -
-
-
-
- Rating -
-
- {roundToHalfOrWhole( - recipe_reviews_aggregate.aggregate.avg.rating - )} - /5 -
-
-
-
- About -
-
{about}
-
-
-
- Instructions -
-
- {instructions} -
-
-
-
+
+
+ {/**/} +
+
+
+

+ Recipe Details +

+

+ Personal details and application. +

-
- - {/**/} -
-
-
-
-

- Recipe Reviews -

+
+
+
+
+ Brew Type +
+
{brew_type}
-
- {recipe_reviews.length === 0 ? ( - 'No recipe reviews available' - ) : ( - - )} +
+
+ Bean Weight +
+
+ {bean_weight}g +
-
- +
+
+ Bean Grind +
+
{bean_grind}
+
+
+
+ Water Amount +
+
+ {water_amount}g +
+
+
+
+ Water Temp +
+
+ {water_temp}F +
+
+
+
Bean
+
+ {bean_name_free} +
+
+
+
+ Rating +
+
+ {roundToHalfOrWhole( + recipe_reviews_aggregate.aggregate.avg.rating + )} + /5 +
+
+
+
About
+
{about}
+
+
+
+ Instructions +
+
+ {instructions} +
+
+
-
-
+
+
+
-
-
-

- Stages -

+
+
+

+ Stages +

- {/**/} + {/**/} -
+ {stages.length > 0 ? ( + <> +
+
+ Go to Recipe Player + + ) : ( +

+ + + + + + This recipe does not contain a playable format +

+ )} + + + + {/**/} +
+
+
+
+

+ Recipe Reviews +

+
+
+ {recipe_reviews.length === 0 ? ( +

+ No recipe reviews available +

+ ) : ( + + )}
-
- - - + {isAuthenticated && user.id !== barista?.id && ( + + )} + + + + ) } diff --git a/src/components/Recipe/Review/CreateRecipeReview.js b/src/components/Recipe/Review/CreateRecipeReview.js index 606ce96..bb34c83 100644 --- a/src/components/Recipe/Review/CreateRecipeReview.js +++ b/src/components/Recipe/Review/CreateRecipeReview.js @@ -3,6 +3,7 @@ import { useMutation } from 'urql' import { INSERT_RECIPE_REVIEW_ONE } from 'queries' import InputRow from 'components/InputRow' import { useAuth } from 'context/AuthContext' +import { PlaceHolder } from 'components/Icon' const CreateRecipeReview = ({ id }) => { const [state, setState] = useState({ @@ -37,11 +38,15 @@ const CreateRecipeReview = ({ id }) => {
- + {barista?.avatar ? ( + + ) : ( + + )}
diff --git a/src/components/Stage/index.js b/src/components/Stage/index.js new file mode 100644 index 0000000..fb1f544 --- /dev/null +++ b/src/components/Stage/index.js @@ -0,0 +1,24 @@ +import { wordCapitalized } from 'helper/stringHelper' + +export const Table = ({ stages }) => ( +
+ + + + + + + + + + {stages.map((stage, i) => ( + + + + + + + ))} + +
ActionStart (s)End (s)Weight (g)
{wordCapitalized(stage.action)}{stage.start}{stage.end}{stage.weight}
+) diff --git a/src/queries/Recipe.js b/src/queries/Recipe.js index 9a874fd..cc03fef 100644 --- a/src/queries/Recipe.js +++ b/src/queries/Recipe.js @@ -94,6 +94,13 @@ const GET_SINGLE_RECIPE_REVIEWS_AVG_REVIEW = gql` name instructions bean_name_free + stages { + id + action + end + start + weight + } barista { id display_name @@ -143,6 +150,13 @@ const GET_SINGLE_RECIPE = gql` name instructions bean_name_free + stages { + id + action + end + start + weight + } bean { img name From 5999aaaaef8c5f1cc57b928ab0a45b621520d504 Mon Sep 17 00:00:00 2001 From: Patrick Truong Date: Fri, 12 Feb 2021 10:04:41 -0800 Subject: [PATCH 14/19] [WIP] Edit functionality; queries and UI Move and rename recipe files Rename and organize recipe verb page add util svg and image add delete modal minor changes to clean up Remove defunct stage editor fix nan error and route error respect newline in instructions & change container style remove old edit form add verbs edit/create components for recipe use new edit form and change delete button rendering [WIP] add fragments --- src/components/Icon/index.js | 15 + src/components/Layout/Container.js | 2 +- src/components/Message/index.js | 28 ++ src/components/Modal/DeleteConfirmation.js | 54 +++ src/components/Modal/Modal.js | 6 + src/components/Player/Timeline.js | 8 +- src/components/Recipe/Create/index.js | 0 src/components/Recipe/Detail/Description.js | 50 +++ src/components/Recipe/Detail/index.js | 109 ++++++ src/components/Recipe/Edit/index.js | 355 ++++++++++++++++++ src/components/Recipe/EditRecipe.js | 20 - src/components/Recipe/EditRecipeForm.js | 147 -------- src/components/Recipe/RecipeDetails.js | 273 -------------- .../Recipe/Review/{RecipeReview.js => All.js} | 4 +- .../{CreateRecipeReview.js => Create.js} | 5 +- src/components/Recipe/Review/index.js | 3 + src/components/StageForm/Row.js | 2 +- src/components/StageForm/index.js | 4 +- src/components/StageInput/ServeTime.js | 36 -- src/components/StageInput/index.js | 126 ------- src/helper/sanitize.js | 50 ++- .../Recipe/CreateRecipe/StageRow.js | 0 .../Recipe/CreateRecipe/index.js | 2 +- src/pages/Recipe/Detail/coffee_cup.jpg | Bin 0 -> 8731 bytes src/pages/Recipe/Detail/index.js | 108 ++++++ src/pages/Recipe/EditRecipe/index.js | 27 ++ .../index.js => Recipe/Player.js} | 14 +- .../Recipe}/Recipe/RecipeCard.js | 12 +- .../Recipe}/Recipe/index.js | 0 .../Recipe/Review/EditRecipeReview.js | 0 .../Recipe/Review/EditRecipeReviewForm.js | 0 src/pages/Recipe/Stage.js | 32 -- src/pages/Recipe/StagePage.js | 48 --- src/pages/Recipe/index.js | 34 +- src/queries/Recipe.js | 44 +++ 35 files changed, 886 insertions(+), 732 deletions(-) create mode 100644 src/components/Message/index.js create mode 100644 src/components/Modal/DeleteConfirmation.js create mode 100644 src/components/Recipe/Create/index.js create mode 100644 src/components/Recipe/Detail/Description.js create mode 100644 src/components/Recipe/Detail/index.js create mode 100644 src/components/Recipe/Edit/index.js delete mode 100644 src/components/Recipe/EditRecipe.js delete mode 100644 src/components/Recipe/EditRecipeForm.js delete mode 100644 src/components/Recipe/RecipeDetails.js rename src/components/Recipe/Review/{RecipeReview.js => All.js} (97%) rename src/components/Recipe/Review/{CreateRecipeReview.js => Create.js} (96%) create mode 100644 src/components/Recipe/Review/index.js delete mode 100644 src/components/StageInput/ServeTime.js delete mode 100644 src/components/StageInput/index.js rename src/{components => pages}/Recipe/CreateRecipe/StageRow.js (100%) rename src/{components => pages}/Recipe/CreateRecipe/index.js (99%) create mode 100644 src/pages/Recipe/Detail/coffee_cup.jpg create mode 100644 src/pages/Recipe/Detail/index.js create mode 100644 src/pages/Recipe/EditRecipe/index.js rename src/pages/{RecipePlayer/index.js => Recipe/Player.js} (52%) rename src/{components => pages/Recipe}/Recipe/RecipeCard.js (94%) rename src/{components => pages/Recipe}/Recipe/index.js (100%) rename src/{components => pages}/Recipe/Review/EditRecipeReview.js (100%) rename src/{components => pages}/Recipe/Review/EditRecipeReviewForm.js (100%) delete mode 100644 src/pages/Recipe/Stage.js delete mode 100644 src/pages/Recipe/StagePage.js diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js index de79514..c0760f9 100644 --- a/src/components/Icon/index.js +++ b/src/components/Icon/index.js @@ -1,6 +1,21 @@ import * as Alert from './Alert' export { Alert } +export const InfoCircleSolid = ({ className = 'h-6 w-6' }) => ( + + + +) + export const PlaceHolder = ({ className = 'h-6 w-6' }) => ( {
diff --git a/src/components/Message/index.js b/src/components/Message/index.js new file mode 100644 index 0000000..9675b0b --- /dev/null +++ b/src/components/Message/index.js @@ -0,0 +1,28 @@ +import { alertType } from 'context/AlertContext' + +export const Message = ({ type = alertType.ERROR, children }) => ( +
+
+ {type === alertType.ERROR && ( +
+ {/* */} + + + +
+ )} +

{children}

+
+
+) diff --git a/src/components/Modal/DeleteConfirmation.js b/src/components/Modal/DeleteConfirmation.js new file mode 100644 index 0000000..981edf0 --- /dev/null +++ b/src/components/Modal/DeleteConfirmation.js @@ -0,0 +1,54 @@ +const DeleteConfirmation = ({ onCancel, onConfirm }) => ( + <> +
+
+ +
+
+ +
+

+ Are you sure you want to delete this recipe? +

+
+
+
+
+ + +
+ +) + +export default DeleteConfirmation diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js index cb4f2c7..60f7230 100644 --- a/src/components/Modal/Modal.js +++ b/src/components/Modal/Modal.js @@ -11,6 +11,7 @@ import { useEffect, useCallback, useRef } from 'react' import Alert from 'components/Alert' import { useAlert } from 'context/AlertContext' import Unverified from './Unverified' +import DeleteConfirmation from './DeleteConfirmation' /** * onClose -> click away or press 'X' button * onSuccess -> login successful, whatever render action should happen now @@ -153,6 +154,11 @@ function Modal() { /> ) : content === 'unverified' ? ( + ) : content === 'delete' ? ( + ) : null}
diff --git a/src/components/Player/Timeline.js b/src/components/Player/Timeline.js index 5667d84..9c71791 100644 --- a/src/components/Player/Timeline.js +++ b/src/components/Player/Timeline.js @@ -9,13 +9,13 @@ const Timeline = ({ stages, stage, seconds }) => (
    - {stages.map(({ name, start, end }, index) => ( + {stages.map(({ action, start, end }, index) => ( = end} /> ))} diff --git a/src/components/Recipe/Create/index.js b/src/components/Recipe/Create/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Recipe/Detail/Description.js b/src/components/Recipe/Detail/Description.js new file mode 100644 index 0000000..286f468 --- /dev/null +++ b/src/components/Recipe/Detail/Description.js @@ -0,0 +1,50 @@ +import { roundToHalfOrWhole } from 'helper/math' + +const Section = ({ className, label, children }) => ( +
    +
    {label}
    +
    {children}
    +
    +) + +export const Description = ({ + brew_type, + bean_weight, + bean_grind, + water_amount, + water_temp, + bean_name_free, + recipe_reviews_aggregate, + about, + instructions, +}) => ( +
    +
    + {brew_type} +
    +
    + {bean_weight}g +
    +
    + {bean_grind} +
    +
    + {water_amount}g +
    +
    + {water_temp}F +
    +
    + {bean_name_free} +
    +
    + {roundToHalfOrWhole(recipe_reviews_aggregate.aggregate.avg.rating)}/5 +
    +
    + {about} +
    +
    + {instructions} +
    +
    +) diff --git a/src/components/Recipe/Detail/index.js b/src/components/Recipe/Detail/index.js new file mode 100644 index 0000000..3e3ea3a --- /dev/null +++ b/src/components/Recipe/Detail/index.js @@ -0,0 +1,109 @@ +import { Link } from 'react-router-dom' +import { InfoCircleSolid } from 'components/Icon' +import { Table } from 'components/Stage' +import { All, Create } from 'components/Recipe/Review' +import { Description } from './Description' + +export const DescriptionSection = ({ recipe }) => ( +
    +
    +
    +

    + Recipe Details +

    +

    + Personal details and application. +

    +
    +
    + +
    +
    +
    +) + +export const ActivitySection = ({ stages, playerPath }) => ( +
    +
    +

    Stages

    + + {stages.length > 0 ? ( + <> +
    + + + + Go to Recipe Player + + + ) : ( +

    + + This recipe does not contain a playable format +

    + )} + + +) + +export const CommentSection = ({ recipeId, recipeReviews, canReview }) => { + return ( +
    +
    +
    +
    +

    + Recipe Reviews +

    +
    +
    + {recipeReviews.length === 0 ? ( +

    + No recipe reviews available +

    + ) : ( + + )} +
    +
    + {canReview && } +
    +
    + ) +} + +export const TitleSection = ({ img, dateAdded, name, recipeName }) => ( +
    + {img.alt} +
    +

    {recipeName}

    +

    + Created by {name} on{' '} + +

    +
    +
    +) + +export const ModifyRow = ({ canModify, onDelete, editPath }) => + canModify ? ( +
    + + + Edit Recipe + +
    + ) : null diff --git a/src/components/Recipe/Edit/index.js b/src/components/Recipe/Edit/index.js new file mode 100644 index 0000000..9e8c7b9 --- /dev/null +++ b/src/components/Recipe/Edit/index.js @@ -0,0 +1,355 @@ +import { useMutation } from 'urql' +import { useReducer } from 'react' +import { useHistory, Link } from 'react-router-dom' +import { useAlert, alertType } from 'context/AlertContext' +import { UPDATE_RECIPES } from 'queries' +import InputRow from 'components/InputRow' +import Dropdown from 'components/DropDown' +import TextArea from 'components/TextArea' +import StageForm from 'components/StageForm' +import StageRow from 'pages/Recipe/CreateRecipe/StageRow' +import { checkSchema, convertEmptyStringToNull } from 'helper/sanitize' + +const recipeType = { + about: { isNullable: true }, + device: { isNullable: true }, + bean_name_free: { isNullable: true }, + brew_type: { isNullable: false }, + bean_weight: { isNullable: false }, + bean_grind: { isNullable: false }, + water_amount: { isNullable: false }, + water_temp: { isNullable: false }, + is_private: { isNullable: false }, + name: { isNullable: false }, + instructions: { isNullable: false }, +} + +const setInitState = ({ stages, ...recipe }) => ({ + form: recipe, + stages, + isVisible: false, + hasIssues: false, +}) + +function reducer(state, [type, payload]) { + switch (type) { + case 'setForm': + return { + ...state, + form: { + ...state.form, + [payload.key]: payload.value, + }, + } + case 'openForm': + return { ...state, isVisible: true } + case 'closeForm': + return { ...state, isVisible: false } + case 'saveStages': + return { ...state, stages: payload } + case 'deleteStages': + return { ...state, stages: null } + case 'setHasIssues': + return { ...state, hasIssues: payload } + default: + return state + } +} + +function Edit({ recipe, id }) { + const history = useHistory() + + const { addAlert, clearAlerts, hasAlerts } = useAlert() + const [{ form, stages, isVisible, hasIssues }, dispatch] = useReducer( + reducer, + setInitState(recipe) + ) + + const [, updateRecipe] = useMutation(UPDATE_RECIPES) + + const onChangeGenerator = (attr) => ({ target }) => { + const { value, type } = target + if (hasIssues) { + dispatch(['setHasIssues', false]) + } + if (hasAlerts) { + clearAlerts() + } + dispatch([ + 'setForm', + { + key: attr, + value: type === 'number' && value !== '' ? parseInt(value) : value, + }, + ]) + } + + const submitRecipe = async (e) => { + e.preventDefault() + const normalized = convertEmptyStringToNull(form) + const { value, errors } = checkSchema(recipeType, normalized) + if (errors) { + dispatch(['setHasIssues', true]) + } else { + // use 'value' instead of 'form' because all empty strings are converted to nulls (SAFER) + // removed 'barista_id' because I grab it from JWT 'x-hasura-barista-id' in Hasura + if (stages) { + value.stages = { data: stages } + } + const { error } = await updateRecipe({ object: value }) + if (error) { + addAlert({ + type: alertType.ERROR, + header: error.message, + close: true, + }) + } else { + history.push(`/recipe/${id}`, { edited: true }) + } + } + } + + const closeForm = () => dispatch(['closeForm']) + const save = (stages) => { + dispatch(['saveStages', stages]) + closeForm() + } + const onDelete = () => { + dispatch(['deleteStages']) + closeForm() + } + + const saveDisabled = hasAlerts || hasIssues || isVisible + + return ( +
    + + {/* Header */} +
    +

    + Create Recipe +

    +

    + Follow the form to list out recipe steps. You may also add playable + recipe steps to use the recipe player. +

    +
    + {/* Form Inputs */} +
    +
    +
    +

    Basics

    +

    + Tell us some details about your recipe +

    +
    +
    + + +
    +
    +
    +
    +

    Bean

    +

    + Add details about your coffee bean +

    +
    +
    + + + +
    +
    +
    +
    +

    Tools

    +

    + What device should we be using? +

    +
    +
    + +
    +
    +
    +
    +

    Brewing

    +

    + How do we create your elixir? +

    +
    +
    + + +