diff --git a/frontend/app/components/book-detail/BookDetailControlButtons.tsx b/frontend/app/components/book-detail/BookDetailControlButtons.tsx new file mode 100644 index 00000000..b5b420f8 --- /dev/null +++ b/frontend/app/components/book-detail/BookDetailControlButtons.tsx @@ -0,0 +1,20 @@ +import BookDetailEditButton from './BookDetailEditButton'; +import { MdDeleteForever } from 'react-icons/md'; +import { Button } from '@mantine/core'; +import { useFetcher } from '@remix-run/react'; +import BookDetailDeleteButton from './BookDetailDeleteButton'; + +interface BookDetailControlButtonsProps { + id: number; +} + +const BookDetailControlButtons = ({ id }: BookDetailControlButtonsProps) => { + return ( + <> + + + + ); +}; + +export default BookDetailControlButtons; diff --git a/frontend/app/components/book-detail/BookDetailControlPanel.tsx b/frontend/app/components/book-detail/BookDetailControlPanel.tsx index c0d4e236..57788c4d 100644 --- a/frontend/app/components/book-detail/BookDetailControlPanel.tsx +++ b/frontend/app/components/book-detail/BookDetailControlPanel.tsx @@ -1,22 +1,27 @@ -import { Button, Stack } from '@mantine/core'; -import { useFetcher } from '@remix-run/react'; +import { Stack } from '@mantine/core'; import { useAtom } from 'jotai'; -import { MdDeleteForever } from 'react-icons/md'; import { userAtom } from '~/stores/userAtom'; -import BookDetailEditButton from './BookDetailEditButton'; import BookDetailThumbnail from './BookDetailThumbnail'; +import BookDetailControlButtons from './BookDetailControlButtons'; +import { useLocation } from '@remix-run/react'; +import GlobalBookDetailControlButtons from '../global-book-detail/GlobalBookDetailControlButtons'; +import { SearchBooks200BooksItem } from 'client/client.schemas'; interface BookDetailControlPanelProps { - id: number; + id?: number; thumbnail?: string; + searchBook?: SearchBooks200BooksItem; + totalBook?: number; } const BookDetailControlPanel = ({ id, thumbnail, + searchBook, + totalBook, }: BookDetailControlPanelProps) => { const [user] = useAtom(userAtom); - const fetcher = useFetcher(); + const location = useLocation(); return ( - {!!user && } - {!!user && ( - - )} + {user && location.pathname.includes('global') + ? searchBook && + typeof totalBook == 'number' && ( + + ) + : id && } ); }; diff --git a/frontend/app/components/book-detail/BookDetailDeleteButton.tsx b/frontend/app/components/book-detail/BookDetailDeleteButton.tsx new file mode 100644 index 00000000..1fd70425 --- /dev/null +++ b/frontend/app/components/book-detail/BookDetailDeleteButton.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { MdDeleteForever } from 'react-icons/md'; +import { Button } from '@mantine/core'; +import { useFetcher } from '@remix-run/react'; + +interface BookDetailDeleteButtonProps { + bookId: number; +} + +const BookDetailDeleteButton = ({ bookId }: BookDetailDeleteButtonProps) => { + const fetcher = useFetcher(); + return ( + + ); +}; + +export default BookDetailDeleteButton; diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailAuthorBadge.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailAuthorBadge.tsx new file mode 100644 index 00000000..da99c194 --- /dev/null +++ b/frontend/app/components/global-book-detail/GlobalBookDetailAuthorBadge.tsx @@ -0,0 +1,24 @@ +import { Badge } from '@mantine/core'; + +interface GlobalBookDetailAuthorBadgeProps { + name: string; +} + +const GlobalBookDetailAuthorBadge = ({ + name, +}: GlobalBookDetailAuthorBadgeProps) => { + return ( + + {name} + + ); +}; + +export default GlobalBookDetailAuthorBadge; diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx new file mode 100644 index 00000000..f321d3ae --- /dev/null +++ b/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx @@ -0,0 +1,32 @@ +import { Stack } from '@mantine/core'; +import { SearchBooks200BooksItem } from 'client/client.schemas'; +import GlobalBookDetailContentTable from './GlobalBookDetailContentTable'; +import BookDetailTitle from '../book-detail/BookDetailTitle'; +import BookDetailDescription from '../book-detail/BookDetailDescription'; +import GlobalBookDetailLink from './GlobalBookDetailLink'; + +interface GlobalBookDetailContentProps { + book: SearchBooks200BooksItem; + bookId?: number; +} + +const GlobalBookDetailContent = ({ + book, + bookId, +}: GlobalBookDetailContentProps) => { + return ( + + + + + {!!bookId && } + + ); +}; + +export default GlobalBookDetailContent; diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx new file mode 100644 index 00000000..28c4564b --- /dev/null +++ b/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx @@ -0,0 +1,43 @@ +import { Group, rem, Stack, Table, Text } from '@mantine/core'; +import { SearchBooks200BooksItem } from 'client/client.schemas'; +import GlobalBookDetailAuthorBadge from './GlobalBookDetailAuthorBadge'; + +interface GlobalBookDetailContentTableProps { + book: SearchBooks200BooksItem; +} + +const GlobalBookDetailContentTable = ({ + book, +}: GlobalBookDetailContentTableProps) => { + return ( + + 書籍情報 + + + 著者 + + + {book.authors.map((author, id) => ( + + ))} + + + + + 出版社 + {book.publisher} + + + 出版日 + {book.publishedDate} + + + ISBN + {book.isbn} + +
+
+ ); +}; + +export default GlobalBookDetailContentTable; diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx new file mode 100644 index 00000000..7fb7ef03 --- /dev/null +++ b/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx @@ -0,0 +1,44 @@ +import { Button } from '@mantine/core'; +import { useFetcher } from '@remix-run/react'; +import { CreateBookBody, SearchBooks200BooksItem } from 'client/client.schemas'; +import { BiSolidBookAdd } from 'react-icons/bi'; + +interface GlobalBookDetailControlButtonsProps { + searchBook: SearchBooks200BooksItem; + totalBook: number; +} + +const GlobalBookDetailControlButtons = ({ + searchBook, + totalBook, +}: GlobalBookDetailControlButtonsProps) => { + const fetcher = useFetcher(); + const addBookData: CreateBookBody = { + authors: searchBook.authors, + description: searchBook.description ?? '', + isbn: searchBook.isbn ?? '', + publishedDate: searchBook.publishedDate ?? '', + publisher: searchBook.publisher ?? '', + stock: 1, + thumbnail: searchBook.thumbnail, + title: searchBook.title, + }; + return ( + + ); +}; + +export default GlobalBookDetailControlButtons; diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailLink.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailLink.tsx new file mode 100644 index 00000000..d78dfcae --- /dev/null +++ b/frontend/app/components/global-book-detail/GlobalBookDetailLink.tsx @@ -0,0 +1,16 @@ +import { Anchor, Text } from '@mantine/core'; + +interface GlobalBookDetailLinkProps { + bookId: number; +} + +const GlobalBookDetailLink = ({ bookId }: GlobalBookDetailLinkProps) => { + return ( + + この書籍はすでに登録されています。蔵書の詳細ページは + こちら + + ); +}; + +export default GlobalBookDetailLink; diff --git a/frontend/app/components/global-books/GlobalBookCard.tsx b/frontend/app/components/global-books/GlobalBookCard.tsx index 5ff51786..fd0facb2 100644 --- a/frontend/app/components/global-books/GlobalBookCard.tsx +++ b/frontend/app/components/global-books/GlobalBookCard.tsx @@ -11,7 +11,8 @@ const GlobalBookCard = ({ book }: GlobalBookCardProps) => { diff --git a/frontend/app/routes/home._index/route.tsx b/frontend/app/routes/home._index/route.tsx index 8378a7ba..e3c638bd 100644 --- a/frontend/app/routes/home._index/route.tsx +++ b/frontend/app/routes/home._index/route.tsx @@ -26,7 +26,7 @@ interface LoaderData { }; } -interface ActionResponse { +export interface ActionResponse { method: string; status: number; } diff --git a/frontend/app/routes/home.global.books.$isbn/route.tsx b/frontend/app/routes/home.global.books.$isbn/route.tsx new file mode 100644 index 00000000..a19ee2b2 --- /dev/null +++ b/frontend/app/routes/home.global.books.$isbn/route.tsx @@ -0,0 +1,128 @@ +import { Grid, rem, Stack } from '@mantine/core'; +import { + ActionFunctionArgs, + json, + LoaderFunctionArgs, + redirect, +} from '@remix-run/cloudflare'; +import { useLoaderData } from '@remix-run/react'; +import { + createBook, + getBooks, + searchBooks, + searchBooksResponse, +} from 'client/client'; +import { CreateBookBody } from 'client/client.schemas'; +import BookDetailControlPanel from '~/components/book-detail/BookDetailControlPanel'; +import GlobalBookDetailContent from '~/components/global-book-detail/GlobalBookDetailContent'; +import { commitSession, getSession } from '~/services/session.server'; +import { ActionResponse } from '../home._index/route'; + +interface LoaderData { + searchBooksResponse: searchBooksResponse; + totalBook?: number; + bookId?: number; +} + +export const loader = async ({ params, request }: LoaderFunctionArgs) => { + const session = await getSession(request.headers.get('Cookie')); + // 書籍の情報を取得する + const isbn = params.isbn ?? ''; + const searchBooksResponse = await searchBooks({ isbn: isbn }); + // 既に登録済みであるか確認するため + if (session.has('userId')) { + const getBookResponse = await getBooks({ isbn: isbn }); + if (getBookResponse.data.totalBook > 0) { + return json({ + searchBooksResponse: searchBooksResponse, + totalBook: getBookResponse.data.totalBook, + bookId: getBookResponse.data.books[0].id, + }); + } else { + return json({ + searchBooksResponse: searchBooksResponse, + totalBook: getBookResponse.data.totalBook, + bookId: undefined, + }); + } + } else { + // ログイン済みでない場合は、APIを呼び出す回数を減らすために蔵書の情報を取得しない + return json({ + searchBooksResponse: searchBooksResponse, + totalBook: undefined, + bookId: undefined, + }); + } +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + const session = await getSession(request.headers.get('Cookie')); + + // 未ログインの場合 + if (!session.has('userId')) { + session.flash('error', 'ログインしてください'); + return redirect('/login', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + } + + const cookieHeader = [ + `__Secure-user_id=${session.get('userId')};`, + `__Secure-session_token=${session.get('sessionToken')}`, + ].join('; '); + + const requestBody = await request.json(); + + const response = await createBook(requestBody, { + headers: { Cookie: cookieHeader }, + }); + + switch (response.status) { + case 201: + session.flash('success', '書籍を追加しました'); + return redirect('/home', { + headers: { 'Set-Cookie': await commitSession(session) }, + }); + case 400: + session.flash('error', 'リクエストの中身が誤っています'); + return json( + { method: 'POST', status: response.status }, + { headers: { 'Set-Cookie': await commitSession(session) } }, + ); + case 401: + session.flash('error', 'ログインしてください'); + return redirect('/login', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + } +}; + +const GlobalBookDetailPage = () => { + const { searchBooksResponse, totalBook, bookId } = + useLoaderData(); + return ( + + + + + + + + + + + ); +}; + +export default GlobalBookDetailPage;