diff --git a/src/components/Auth/Auth.styled.tsx b/src/components/Auth/Auth.styled.tsx index 7cf8f4a..c7bc432 100644 --- a/src/components/Auth/Auth.styled.tsx +++ b/src/components/Auth/Auth.styled.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; -import { pxToRem } from 'utils'; +import { media, pxToRem } from 'utils'; +import { Button } from 'components'; import { StyledInputProps } from './Auth.types'; export const StyledAuthContainer = styled.div` @@ -10,7 +11,6 @@ export const StyledAuthContainer = styled.div` export const StyledForm = styled.form` margin: ${pxToRem(18)} auto; - min-width: ${pxToRem(280)}; display: flex; flex-direction: column; gap: ${pxToRem(4)}; @@ -25,12 +25,12 @@ export const StyledForm = styled.form` export const StyledAuthError = styled.div` border: 2px solid red; - background-color: rgba(255, 0, 0, 0.4); + background-color: rgba(255, 0, 0, 0.7); border-radius: ${pxToRem(5)}; padding: ${pxToRem(16)} 0; margin: ${pxToRem(18)} auto; max-width: ${pxToRem(400)}; - min-width: ${pxToRem(280)}; + ${media.mobile && 'padding: 20px;'} text-align: center; color: white; `; @@ -49,4 +49,13 @@ export const StyledInput = styled.input` border: none; border-radius: ${pxToRem(5)} ${pxToRem(5)}; ${({ $warning, theme }) => $warning && `box-shadow: 0 0 1px 5px ${theme.color.primaryOrange};`} + background-color: ${({ theme }) => theme.color.searchGray}; +`; + +export const StyledToggleButton = styled(Button)` + text-align: center; + width: 100%; + :hover { + text-decoration: underline; + } `; diff --git a/src/components/Auth/Auth.tsx b/src/components/Auth/Auth.tsx index cef4108..ec661d6 100644 --- a/src/components/Auth/Auth.tsx +++ b/src/components/Auth/Auth.tsx @@ -1,6 +1,17 @@ +import { actions } from 'store/slices/auth'; +import { Button, Heading, Dialog } from 'components'; +import { useState, Fragment } from 'react'; +import { useDispatch } from 'react-redux'; import { FormikProps, withFormik } from 'formik'; -import { FormValues, FormProps, AuthContainerProps } from './Auth.types'; -import { StyledForm, StyledInput, StyledAuthError, StyledFieldError, StyledAuthContainer } from './Auth.styled'; +import { FormValues, FormProps, AuthContainerProps, Form } from './Auth.types'; +import { + StyledForm, + StyledInput, + StyledAuthError, + StyledFieldError, + StyledAuthContainer, + StyledToggleButton, +} from './Auth.styled'; import { AUTH_STATE, AUTH_FUNC, @@ -11,11 +22,8 @@ import { PLACEHOLDER, TYPE, AUTH_ERROR_MSG, + TOGGLE_MESSAGE, } from './AuthServices'; -import { Button, Heading } from 'components'; -import { useState, Fragment } from 'react'; -import { useDispatch } from 'react-redux'; -import { actions } from 'store/slices/auth'; const AuthForm = (props: FormProps & FormikProps): JSX.Element => { const { currentForm, values, errors, dirty, touched, isValid, handleChange, handleBlur, handleSubmit } = props; @@ -43,7 +51,7 @@ const AuthForm = (props: FormProps & FormikProps): JSX.Element => { ), )} - @@ -58,26 +66,36 @@ const Auth = withFormik({ }, })(AuthForm); -export const AuthContainer = ({ onClose }: AuthContainerProps) => { - const [currentForm, setCurrentForm] = useState(AUTH_STATE.signin); +export const AuthContainer = ({ onClose, onSignIn }: AuthContainerProps) => { + const [currentForm, setCurrentForm] = useState
(AUTH_STATE.signin); const [hasAuthError, setAuthError] = useState(false); const dispatch = useDispatch(); - const handleSubmit = async (values) => { + const toggleCurrentForm = () => { + setCurrentForm(currentForm === AUTH_STATE.signin ? AUTH_STATE.signup : AUTH_STATE.signin); + }; + + const handleSubmit = async (values: FormValues) => { try { dispatch(actions.loading(true)); const { uid: userId } = await AUTH_FUNC[currentForm](values); dispatch(actions.signIn(userId)); onClose(); + onSignIn(); } catch (e) { setAuthError(true); } }; return ( - - {HEADING[currentForm]} - {hasAuthError && {AUTH_ERROR_MSG[currentForm]}} - - + + + {HEADING[currentForm]} + {hasAuthError && {AUTH_ERROR_MSG[currentForm]}} + + + {TOGGLE_MESSAGE[currentForm]} + + + ); }; diff --git a/src/components/Auth/Auth.types.ts b/src/components/Auth/Auth.types.ts index 2e7f7ca..58d1d45 100644 --- a/src/components/Auth/Auth.types.ts +++ b/src/components/Auth/Auth.types.ts @@ -1,3 +1,5 @@ +export type Form = 'signin' | 'signup'; + export interface SignInFormValues { password: string; email: string; @@ -16,7 +18,7 @@ export interface FormProps { initialPasswordConfirm?: string; initialUsername?: string; currentForm: 'signin' | 'signup'; - onSubmit: (values: {}) => void; + onSubmit: (values: FormValues) => void; } export interface StyledInputProps { @@ -26,4 +28,5 @@ export interface StyledInputProps { export interface AuthContainerProps { onClose: () => void; + onSignIn: () => void; } \ No newline at end of file diff --git a/src/components/Dialog/Dialog.stories.tsx b/src/components/Dialog/Dialog.stories.tsx index 32f1740..7eb3313 100644 --- a/src/components/Dialog/Dialog.stories.tsx +++ b/src/components/Dialog/Dialog.stories.tsx @@ -1,5 +1,4 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { AuthContainer } from 'components'; import { Dialog } from './Dialog'; export default { @@ -16,11 +15,6 @@ export default { const Template: ComponentStory = (args) => ; export const DefaultDialog = Template.bind({}); - -export const AuthDialog = Template.bind({}); - -AuthDialog.args = { ...DefaultDialog.args, children: }; - export const LongDialog = Template.bind({}); LongDialog.args = { ...DefaultDialog.args, diff --git a/src/components/Dialog/Dialog.styled.tsx b/src/components/Dialog/Dialog.styled.tsx index 0609068..995e540 100644 --- a/src/components/Dialog/Dialog.styled.tsx +++ b/src/components/Dialog/Dialog.styled.tsx @@ -1,23 +1,23 @@ import styled from '@emotion/styled'; import { IconButton } from 'components'; -import { media } from 'utils'; +import { media, pxToRem } from 'utils'; export const StyledDialogContainer = styled.div` z-index: 200; - position: absolute; + position: fixed; top: 50vh; left: 50%; ${media.desktop} { - width: 60vw; + width: ${pxToRem(400)}; } ${media.mobile} { - width: 90vw; + width: ${pxToRem(300)}; } min-height: 50vh; max-height: 80vh; transform: translate(-50%, -50%); overflow: auto; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgb(255, 255, 255); `; export const StyledDialogContent = styled.div` @@ -28,14 +28,14 @@ export const StyledDialogContent = styled.div` export const StyledCloseButton = styled(IconButton)` cursor: pointer; - position: absolute; + position: fixed; z-index: 200; top: 5px; right: 5px; border: 0; padding: 5px; background: transparent; - color: #fefefe; + color: ${({theme}) => theme.color.gray400}; svg { pointer-events: none; fill: currentColor; @@ -43,7 +43,7 @@ export const StyledCloseButton = styled(IconButton)` `; export const StyledDim = styled.div` - position: absolute; + position: fixed; z-index: 100; top: 0; left: 0; diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index d953e31..2693c8e 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -21,7 +21,7 @@ export function Dialog({ onClose, children, nodeId = 'dialog', label, ...restPro const lastTabbableElement = tabbableElements[tabbableElements.length - 1]; firstTabbableElement.focus(); - let eventType = 'keydown'; + const eventType = 'keydown'; const eventListener = (e: KeyboardEvent) => { const { key, shiftKey, target } = e; @@ -69,11 +69,11 @@ export function Dialog({ onClose, children, nodeId = 'dialog', label, ...restPro variant="transparent" color="white" size="large" - onClick={onClose} + onClick={handleClose} /> - + , - document.getElementById(nodeId), + document.getElementById(nodeId)!, ); } diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index d949d5c..df1bd58 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,12 +1,15 @@ import React from 'react'; +import Link from 'next/link'; import { Heading, Header, EmptyPage } from '..'; import { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary.types'; -import Link from 'next/link'; export class ErrorBoundary extends React.Component { - public state: ErrorBoundaryState = { - hasError: false, - }; + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + }; + } public componentDidCatch(error: Error) { this.setState({ @@ -18,7 +21,8 @@ export class ErrorBoundary extends React.Component
@@ -31,6 +35,7 @@ export class ErrorBoundary extends React.Component ); } - return this.props.children; + const { children } = this.props; + return children; } } diff --git a/src/components/ErrorBoundary/ErrorBoundary.types.ts b/src/components/ErrorBoundary/ErrorBoundary.types.ts index 8438c94..b7a693a 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.types.ts +++ b/src/components/ErrorBoundary/ErrorBoundary.types.ts @@ -1,6 +1,6 @@ import React from 'react'; -export interface ErrorBoundaryProps extends WithRouterProps { +export interface ErrorBoundaryProps { children?: React.ReactNode; } diff --git a/src/components/Header/Header.styled.tsx b/src/components/Header/Header.styled.tsx index 03802d8..5196bbf 100644 --- a/src/components/Header/Header.styled.tsx +++ b/src/components/Header/Header.styled.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { IconButton } from 'components'; -import { StyledHeaderProps } from './Header.types'; import { HEADER_HEIGHT } from 'styles/GlobalStyle'; import { pxToRem } from 'utils'; +import { StyledHeaderProps } from './Header.types'; export const StyledHeader = styled.header` background-color: ${({ theme }) => theme.color.white}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index ac52140..ca7db9c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,14 +1,18 @@ -import { SearchForm, Menu, Button, Logo, AuthContainer, Dialog } from 'components'; -import { useState, useEffect, useRef } from 'react'; -import lodash from 'lodash'; +import { AuthContainer, Button, Logo, Menu, SearchForm, Toast } from 'components'; +import { useToast } from 'hooks'; +import _ from 'lodash'; +import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { StyledHeader, StyledDiv, StyledIconButton, headerHeight } from './Header.styled'; import { useSelector } from 'react-redux'; import { RootState } from 'store'; import { AuthState } from 'store/slices/auth'; +import { HEADER_HEIGHT } from 'styles/GlobalStyle'; +import { StyledDiv, StyledHeader, StyledIconButton } from './Header.styled'; export const Header = (): JSX.Element => { const [showDialog, setShowDialog] = useState(false); + const { showToast: showSignInToast, displayToast: displaySignInToast } = useToast(2000); + const { showToast: showSignOutToast, displayToast: displaySignOutToast } = useToast(2000); const [hideHeader, setHideHeader] = useState(false); const [showScrollToTop, setShowScrollToTop] = useState(false); const oldScrollTop = useRef(0); @@ -27,16 +31,16 @@ export const Header = (): JSX.Element => { }; const handleBlur = () => { - setHideHeader(window.pageYOffset > headerHeight); + setHideHeader(window.pageYOffset > HEADER_HEIGHT); }; - const controlHeader = lodash.throttle(() => { + const controlHeader = _.throttle(() => { const currentScrollTop = window.pageYOffset; - setHideHeader(currentScrollTop > headerHeight && currentScrollTop > oldScrollTop.current); + setHideHeader(currentScrollTop > HEADER_HEIGHT && currentScrollTop > oldScrollTop.current); oldScrollTop.current = currentScrollTop; }, 300); - const controlScrollToTop = lodash.debounce(() => { + const controlScrollToTop = _.debounce(() => { const currentScrollTop = window.pageYOffset; setShowScrollToTop(currentScrollTop > 500); }, 300); @@ -56,7 +60,7 @@ export const Header = (): JSX.Element => { {authUser ? ( - + ) : ( <> - {showDialog && ( - - - - )} + {showDialog && } )} {showScrollToTop && @@ -97,6 +97,8 @@ export const Header = (): JSX.Element => { />, document.getElementById('__next')!, )} + {showSignInToast && } + {showSignOutToast && } ); diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index e126162..a726b46 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,15 +1,15 @@ -import { useState, useEffect } from 'react'; -import { StyledNav, StyledUl, StyledLi } from './Menu.styled'; +import { useState } from 'react'; import { useRouter } from 'next/router'; -import { IconButton, Button } from 'components'; +import { actions } from 'store/slices/auth'; +import { IconButton, Button, Toast } from 'components'; import Link from 'next/link'; import { logOut } from 'api/requestAuth'; import { useDispatch } from 'react-redux'; -import { actions } from 'store/slices/auth'; +import { StyledNav, StyledUl, StyledLi } from './Menu.styled'; -export const Menu = () => { +export const Menu = ({ onSignOut }) => { const [isOpen, setIsOpen] = useState(false); -const dispatch = useDispatch(); + const dispatch = useDispatch(); const handleClick = () => { setIsOpen(!isOpen); }; @@ -23,7 +23,8 @@ const dispatch = useDispatch(); const handleSignOut = () => { logOut(); dispatch(actions.signOut()); - } + onSignOut(); + }; const router = useRouter(); /* diff --git a/src/components/Menu/Menu.types.ts b/src/components/Menu/Menu.types.ts new file mode 100644 index 0000000..813e616 --- /dev/null +++ b/src/components/Menu/Menu.types.ts @@ -0,0 +1,3 @@ +export interface MenuProps { + onSignOut: () => void; +} \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index d927acb..875466a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,4 +17,5 @@ export * from './ErrorBoundary/ErrorBoundary'; export * from './Card/Card'; export * from './RandomRecipe/RandomRecipe'; export * from './HotRecipes/HotRecipes'; -export * from './Accordion/Accordion'; +export * from './Toast/Toast'; +export * from './Accordion/Accordion'; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3604ec7..33f1079 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './usePageNum'; export * from './useRandomRecipe'; export * from './useHotRecipes'; +export * from './useToast'; \ No newline at end of file diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 0000000..dd73d72 --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,14 @@ +import { useState, useEffect, useCallback } from 'react'; + +export const useToast = (duration = 1000) => { + const [showToast, setShowToast] = useState(false); + + useEffect(() => { + if (showToast) setTimeout(() => setShowToast(false), duration); + }, [showToast]); + + const displayToast = useCallback(() => { + setShowToast(true); + }, []) + return { showToast, displayToast }; +}; diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index 823b3a4..94dc704 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -1,7 +1,13 @@ import NextErrorComponent from 'next/error'; +import { NextPage } from 'next'; +import type { ErrorProps } from 'next/error'; import * as Sentry from '@sentry/nextjs'; -const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => { +interface AppErrorProps extends ErrorProps { + err?: Error; + hasGetInitialPropsRun?: boolean; +} +const MyError: NextPage = ({ statusCode, hasGetInitialPropsRun, err }) => { if (!hasGetInitialPropsRun && err) { // getInitialProps is not called in case of // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass @@ -14,19 +20,17 @@ const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => { }; MyError.getInitialProps = async (context) => { - const errorInitialProps = await NextErrorComponent.getInitialProps(context); - + const errorInitialProps: AppErrorProps = await NextErrorComponent.getInitialProps(context); + const { res, err, asPath } = context; - // Workaround for https://github.com/vercel/next.js/issues/8592, mark when - // getInitialProps has run errorInitialProps.hasGetInitialPropsRun = true; // Returning early because we don't want to log 404 errors to Sentry. if (res?.statusCode === 404) { return errorInitialProps; } - + // Running on the server, the response object (`res`) is available. // // Next.js will pass an err on the server if a page's data fetching methods @@ -53,9 +57,7 @@ MyError.getInitialProps = async (context) => { // If this point is reached, getInitialProps was called without any // information about what the error might be. This is unexpected and may // indicate a bug introduced in Next.js, so record it in Sentry - Sentry.captureException( - new Error(`_error.js getInitialProps missing data at path: ${asPath}`), - ); + Sentry.captureException(new Error(`_error.js getInitialProps missing data at path: ${asPath}`)); await Sentry.flush(2000); return errorInitialProps; diff --git a/src/pages/errorTest.tsx b/src/pages/errorTest.tsx index 99fa8d9..a1e8ac3 100644 --- a/src/pages/errorTest.tsx +++ b/src/pages/errorTest.tsx @@ -1,21 +1,21 @@ import { NextPage } from 'next'; -import { Button, Heading } from 'components'; +import { Button, Heading, EmptyPage } from 'components'; const ErrorTest: NextPage = () => { return ( - <> + Normal Page - + ); }; diff --git a/src/store/slices/auth.ts b/src/store/slices/auth.ts index 9863eb3..1cb8171 100644 --- a/src/store/slices/auth.ts +++ b/src/store/slices/auth.ts @@ -1,17 +1,20 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface AuthState { - isLoading: boolean; authUser: null | string; + isLoading: boolean; } const AuthSlice = createSlice({ name: 'Auth', - initialState: { isLoading: false, authUser: null }, + initialState: { authUser: null, isLoading: false } as AuthState, reducers: { - loading: (state: AuthState, action: PayloadAction) => ({ ...state, isLoading: action.payload }), - signIn: (state: AuthState, action: PayloadAction) => ({ authUser: action.payload, isLoading: false }), - signOut: (state: AuthState) => ({ authUser: null, isLoading: false }), + loading: (state: AuthState, action: PayloadAction) => ({ + ...state, + isLoading: action.payload, + }), + signIn: (_, action: PayloadAction) => ({ authUser: action.payload, isLoading: false }), + signOut: () => ({ authUser: null, isLoading: false }), }, });