Skip to content
Merged
3 changes: 3 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const nextConfig = {

return config;
},
images: {
domains: ['spoonacular.com'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nextjs.org/docs/messages/next-image-unconfigured-host

unfigured host 문제로 config 파일 수정했어요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nextjs.org/docs/basic-features/image-optimization#remote-images

외부 서버 이미지 가져오는 경우에 대한 공식문서 자료도 읽어보면 도움이 되네요!

},
};

const sentryWebpackPluginOptions = {
Expand Down
18 changes: 16 additions & 2 deletions src/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import React from 'react';
import Link from 'next/link';
import { withRouter } from 'next/router';
import { Heading, Header, EmptyPage } from '..';
import { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary.types';

export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
};
}

public componentDidUpdate(nextProps: ErrorBoundaryProps) {
const { pathname: nextPath } = nextProps.router;
const { pathname: currentPath } = this.props.router;

if (currentPath === '/' && currentPath !== nextPath) {
this.setState({
hasError: false,
});
}
}

public componentDidCatch(error: Error) {
this.setState({
hasError: true,
});

console.error('====================================');
console.error(error);
console.error('====================================');
Expand All @@ -39,3 +51,5 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
return children;
}
}

export const ErrorBoundaryWithRouter = withRouter(ErrorBoundary);
8 changes: 6 additions & 2 deletions src/components/ErrorBoundary/ErrorBoundary.types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react';
import { NextRouter } from 'next/router';

export interface ErrorBoundaryProps {
interface WithRouterProps {
router: NextRouter;
}
export interface ErrorBoundaryProps extends WithRouterProps {
children?: React.ReactNode;
}

export interface ErrorBoundaryState {
hasError: boolean;
}
}
17 changes: 14 additions & 3 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useToast } from 'hooks';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'store';
import { AuthState } from 'store/slices/auth';
import { AuthState, actions } from 'store/slices/auth';
import { HEADER_HEIGHT } from 'styles/GlobalStyle';
import { getAuthStatus } from 'api/requestAuth';
import { StyledDiv, StyledHeader, StyledIconButton } from './Header.styled';

export const Header = (): JSX.Element => {
Expand All @@ -17,6 +18,16 @@ export const Header = (): JSX.Element => {
const [showScrollToTop, setShowScrollToTop] = useState(false);
const oldScrollTop = useRef(0);
const { authUser, isLoading } = useSelector<RootState, AuthState>((state) => state.auth);
const dispatch = useDispatch();

useEffect(() => {
(async () => {
dispatch(actions.loading(true));
const user = await getAuthStatus();
if (user) dispatch(actions.signIn(user.uid));
else dispatch(actions.loading(false));
})();
}, []);

const handleOpenDialog = () => {
setShowDialog(true);
Expand Down Expand Up @@ -55,7 +66,7 @@ export const Header = (): JSX.Element => {
}, []);

return (
<StyledHeader onFocus={handleFocus} onBlur={handleBlur} $hide={hideHeader && isLoading}>
<StyledHeader onFocus={handleFocus} onBlur={handleBlur} $hide={hideHeader || isLoading}>
<StyledDiv>
<Logo />
<SearchForm />
Expand Down
29 changes: 14 additions & 15 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { actions } from 'store/slices/auth';
import { IconButton, Button, Toast } from 'components';
import { IconButton, Button } from 'components';
import Link from 'next/link';
import { logOut } from 'api/requestAuth';
import { useDispatch } from 'react-redux';
import { StyledNav, StyledUl, StyledLi } from './Menu.styled';
import { MenuProps } from './Menu.types';

export const Menu = ({ onSignOut }) => {
export const Menu = ({ onSignOut }: MenuProps) => {
const [isOpen, setIsOpen] = useState(false);
const dispatch = useDispatch();
const handleClick = () => {
Expand All @@ -25,22 +26,20 @@ export const Menu = ({ onSignOut }) => {
dispatch(actions.signOut());
onSignOut();
};
const router = useRouter();

/*
TODO: 스토리북에서 next.js 설정 후 주석 해제
useEffect(() => {
const handleRouteChange = () => {
setIsOpen(false);
};
// const router = useRouter();

// useEffect(() => {
// const handleRouteChange = () => {
// setIsOpen(false);
// };

router.events.on('routeChangeStart', handleRouteChange);
// router.events.on('routeChangeStart', handleRouteChange);

return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, []);
*/
// return () => {
// router.events.off('routeChangeStart', handleRouteChange);
// };
// }, []);

return (
<StyledNav onBlur={handleBlur}>
Expand Down
7 changes: 4 additions & 3 deletions src/components/Pagination/Pagination.styled.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { pxToRem } from 'utils';
import { PageButtonProps } from './Pagination.types';

export const StyledPaginationControl = styled.div`
display: flex;
Expand All @@ -14,11 +15,11 @@ export const StyledPaginationControl = styled.div`
}
`;

export const StyledPageButton = styled.button`
color: ${({ theme, current }) => (current ? theme.color.white : theme.color.primaryOrange)};
export const StyledPageButton = styled.button<PageButtonProps>`
color: ${({ theme, $current }) => ($current ? theme.color.white : theme.color.primaryOrange)};
border: 1px solid ${({ theme }) => theme.color.primaryOrange};
border-radius: ${pxToRem(5)};
background-color: ${({ theme, current }) => (current ? theme.color.primaryOrange : theme.color.white)};
background-color: ${({ theme, $current }) => ($current ? theme.color.primaryOrange : theme.color.white)};
width: ${pxToRem(32)};
height: ${pxToRem(32)};
margin: ${pxToRem(6)};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Pagination = ({ limit, currentPage, onClick: handleClick, totalResu
<li key={index}>
<StyledPageButton
type="button"
current={currentPage === pageStartNum + index}
$current={currentPage === pageStartNum + index}
aria-pressed={currentPage === pageStartNum + index}
aria-label={`Go to page ${pageStartNum + index}`}
onClick={() => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Pagination/Pagination.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ export interface PaginationProps {
onClick: (currentPage: number) => void;
totalResults: number;
limit: number;
}

export interface PageButtonProps {
$current?: boolean;
}
6 changes: 5 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ export * from './Card/Card';
export * from './RandomRecipe/RandomRecipe';
export * from './HotRecipes/HotRecipes';
export * from './Toast/Toast';
export * from './Accordion/Accordion';
<<<<<<< HEAD
export * from './Pagination/Pagination';
=======
export * from './Accordion/Accordion';
>>>>>>> 36b63b6fc38e7338bbe3107bb996b543f71a0a79
6 changes: 3 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { ThemeProvider } from '@emotion/react';
import { theme } from 'theme/theme';
import { GlobalStyle } from 'styles/GlobalStyle';
import { StoreProvider } from 'store';
import { Layout, ErrorBoundary } from 'components';
import { Layout, ErrorBoundaryWithRouter } from 'components';

function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={theme}>
<StoreProvider>
<GlobalStyle />
<ErrorBoundary>
<ErrorBoundaryWithRouter>
<Layout>
<Component {...pageProps} />
</Layout>
</ErrorBoundary>
</ErrorBoundaryWithRouter>
</StoreProvider>
</ThemeProvider>
);
Expand Down
52 changes: 35 additions & 17 deletions src/pages/search/[keyword].tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
import { useRouter } from 'next/router';
import { Loading, Pagination } from 'components';
import { NextPage } from 'next';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { useSearchRecipeQuery } from 'store/services';
import { useState } from 'react';
import { SearchRecipeItem } from 'store/services/types/queries';
import { ContextProp, SearchPageProps } from './search.types';

const RESULTS_PER_PAGE = 12;

const Search: NextPage = ({ results, totalResults }: SearchPageProps) => {
const Search: NextPage<SearchPageProps> = ({ keyword, results, totalResults }) => {
const [currentPage, setCurrentPage] = useState(1);
const [currentResults, setCurrentResults] = useState<SearchRecipeItem[]>([]);
const { data, isFetching } = useSearchRecipeQuery({
keyword,
number: RESULTS_PER_PAGE,
offset: (currentPage - 1) * RESULTS_PER_PAGE,
});

useEffect(() => {
if (currentPage !== 1 && data) setCurrentResults(data.results);
}, [data]);

// const {
// query: { keyword },
// } = useRouter();
// const [currentIndex, setCurrentIndex] = useState(0);
// const { data, error, isLoading } = useSearchRecipeQuery({
// keyword,
// number: RESULTS_PER_PAGE,
// offset: (currentIndex - 1) * RESULTS_PER_PAGE,
// });
// console.log(data);
const handleClick = (page: number) => {
setCurrentPage(page);
};
return (
<div>
<h1>{totalResults}</h1>
<Head>
<title>{`Searched: ${keyword}`}</title>
</Head>
{currentPage !== 1 && isFetching && (
<Loading message={`Loading ${currentPage} page of search results for ${keyword}`} showBackground />
)}
<ul>
{results.map(({ id, title }) => (
{(currentPage === 1 ? results : currentResults).map(({ id, title, image }) => (
<li key={id}>{title}</li>
))}
</ul>
<Pagination
currentPage={currentPage}
limit={RESULTS_PER_PAGE}
totalResults={totalResults}
onClick={handleClick}
/>
</div>
);
};

export async function getServerSideProps({ query }: ContextProp) {
const { keyword } = query;
const { results, totalResults } = await fetch(
`https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com//recipes/search?query=${keyword}&number=${RESULTS_PER_PAGE}&offset=${0}`,
`https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/search?query=${keyword}&number=${RESULTS_PER_PAGE}&offset=${0}`,
{
headers: {
'content-type': 'application/json',
Expand All @@ -42,7 +60,7 @@ export async function getServerSideProps({ query }: ContextProp) {
} as RequestInit,
).then((res) => res.json());
return {
props: { results, totalResults },
props: { keyword, results, totalResults },
};
}

Expand Down
1 change: 1 addition & 0 deletions src/pages/search/search.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface SearchResult {
}

export interface SearchPageProps {
keyword: string;
results: SearchResult[];
totalResults: number;
}
7 changes: 5 additions & 2 deletions src/store/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { RandomRecipeQuery } from './types/queries';
import { RandomRecipeQuery, SearchQuery, SearchResults } from './types/queries';

export const twoSpoonApi = createApi({
reducerPath: 'twoSpoonApi',
Expand All @@ -17,7 +17,10 @@ export const twoSpoonApi = createApi({
getRandomRecipe: builder.query<RandomRecipeQuery, number>({
query: (number = 1) => `recipes/random?number=${number}`,
}),
searchRecipe: builder.query<SearchResults, SearchQuery>({
query: ({ keyword, number, offset }) => `recipes/search?query=${keyword}&number=${number}&offset=${offset}`,
}),
}),
});

export const { useGetRandomRecipeQuery } = twoSpoonApi;
export const { useGetRandomRecipeQuery, useSearchRecipeQuery } = twoSpoonApi;
25 changes: 25 additions & 0 deletions src/store/services/types/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,28 @@ export interface RandomRecipe {
export interface RandomRecipeQuery {
recipes: RandomRecipe[];
}

export interface SearchRecipeItem {
id: number;
title: string;
readyInMinutes: number;
image: string;
imageUrls: string[];
}

export interface SearchResults {
results: SearchRecipeItem[];
baseUri: string;
offset: number;
number: number;
totalResults: number;
processingTimeMs: number;
expires: number;
isStale: boolean;
}

export interface SearchQuery {
keyword: string;
number: number;
offset: number;
}