diff --git a/frontend/app/components/book-detail/BookDetailControlButtons.tsx b/frontend/app/components/book-detail/BookDetailControlButtons.tsx index b5b420f8..300e30b8 100644 --- a/frontend/app/components/book-detail/BookDetailControlButtons.tsx +++ b/frontend/app/components/book-detail/BookDetailControlButtons.tsx @@ -1,7 +1,4 @@ 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 { diff --git a/frontend/app/components/book-detail/BookDetailDeleteButton.tsx b/frontend/app/components/book-detail/BookDetailDeleteButton.tsx index 1fd70425..ddb988d0 100644 --- a/frontend/app/components/book-detail/BookDetailDeleteButton.tsx +++ b/frontend/app/components/book-detail/BookDetailDeleteButton.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { MdDeleteForever } from 'react-icons/md'; import { Button } from '@mantine/core'; import { useFetcher } from '@remix-run/react'; diff --git a/frontend/app/components/books/BookListComponent.tsx b/frontend/app/components/books/BookListComponent.tsx index 63ae8640..5b154c92 100644 --- a/frontend/app/components/books/BookListComponent.tsx +++ b/frontend/app/components/books/BookListComponent.tsx @@ -7,6 +7,7 @@ import ErrorComponent from '../common/error/ErrorComponent'; import ContentsHeader from '../common/pagination/ContentsHeader'; import PaginationComponent from '../common/pagination/PaginationComponent'; import BookCards from './BookCards'; +import { PaginationProps } from '~/types/paginatiion'; interface BookListComponentProps { booksResponse: getBooksResponse; @@ -18,11 +19,7 @@ interface BookListComponentProps { isOpen: boolean; open: () => void; close: () => void; - handlePaginationChange: (newPage: number) => void; - handleLimitChange: (newLimit: number) => void; - page?: number; - limit?: number; - totalBook: number; + paginationProps: PaginationProps; } const BookListComponent = ({ @@ -32,11 +29,7 @@ const BookListComponent = ({ isOpen, open, close, - handlePaginationChange, - handleLimitChange, - page, - limit, - totalBook, + paginationProps, }: BookListComponentProps) => { return ( @@ -48,10 +41,10 @@ const BookListComponent = ({ handleSubmit={handleSubmit} /> {booksResponse.status !== 200 ? ( @@ -59,10 +52,10 @@ const BookListComponent = ({ )} ); diff --git a/frontend/app/components/common/pagination/PaginationComponent.tsx b/frontend/app/components/common/pagination/PaginationComponent.tsx index fec1b058..f9c8cadc 100644 --- a/frontend/app/components/common/pagination/PaginationComponent.tsx +++ b/frontend/app/components/common/pagination/PaginationComponent.tsx @@ -1,7 +1,7 @@ import { Center, Pagination } from '@mantine/core'; interface PaginationComponentProps { - totalNum: number; + total: number; page?: number; limit?: number; handlePaginationChange: (newPage: number) => void; @@ -9,14 +9,14 @@ interface PaginationComponentProps { } const PaginationComponent = ({ - totalNum, + total, page, limit, handlePaginationChange, color, }: PaginationComponentProps) => { const limitNum = limit ?? 10; - const totalPage = totalNum / limitNum + 1; + const totalPage = total != 0 ? Math.ceil(total / limitNum) : 1; return (
void; handleKeywordSubmit: (props: SearchBooksParams) => void; } -interface HandlePaginationProps { - handlePaginationChange: (newPage: number) => void; - handleLimitChange: (newLimit: number) => void; - page?: number; - limit?: number; - totalBook: number; -} - interface GlobalBookListComponentProps { booksResponse?: searchBooksResponse; form: UseFormReturnType< @@ -30,7 +23,7 @@ interface GlobalBookListComponentProps { (values: SearchBooksParams) => SearchBooksParams >; globalSearchFunctions: HandleGlobalSearchFunctions; - paginationProps: HandlePaginationProps; + paginationProps: PaginationProps; isOpen: boolean; open: () => void; close: () => void; @@ -63,7 +56,7 @@ const GlobalBookListComponent = ({ {!booksResponse ? ( @@ -74,7 +67,7 @@ const GlobalBookListComponent = ({ )} { + return ( +
+
} mt="xl"> + ユーザーが存在しません。 +
+
+ ); +}; + +export default NoUserComponent; diff --git a/frontend/app/components/users/UserAddButton.tsx b/frontend/app/components/users/UserAddButton.tsx new file mode 100644 index 00000000..66efae5e --- /dev/null +++ b/frontend/app/components/users/UserAddButton.tsx @@ -0,0 +1,19 @@ +import { Button } from '@mantine/core'; +import { RiUserAddFill } from 'react-icons/ri'; + +const UserAddButton = () => { + return ( + + ); +}; + +export default UserAddButton; diff --git a/frontend/app/components/users/UserDeleteButton.tsx b/frontend/app/components/users/UserDeleteButton.tsx new file mode 100644 index 00000000..7a914385 --- /dev/null +++ b/frontend/app/components/users/UserDeleteButton.tsx @@ -0,0 +1,24 @@ +import { Button } from '@mantine/core'; + +interface UserDeleteButtonProps { + id: number; + handleDeleteUserButtonClick: (id: number) => void; +} + +const UserDeleteButton = ({ + id, + handleDeleteUserButtonClick, +}: UserDeleteButtonProps) => { + return ( + + ); +}; + +export default UserDeleteButton; diff --git a/frontend/app/components/users/UsersListComponent.tsx b/frontend/app/components/users/UsersListComponent.tsx new file mode 100644 index 00000000..45d51b2c --- /dev/null +++ b/frontend/app/components/users/UsersListComponent.tsx @@ -0,0 +1,57 @@ +import { Container, Stack } from '@mantine/core'; +import UsersListTitle from './UsersListTitle'; +import ContentsHeader from '../common/pagination/ContentsHeader'; +import type { PaginationProps } from '~/types/paginatiion'; +import PaginationComponent from '../common/pagination/PaginationComponent'; +import ErrorComponent from '../common/error/ErrorComponent'; +import { getUsersResponse } from 'client/client'; +import UsersListTable from './UsersListTable'; +import UserAddButton from './UserAddButton'; + +interface UsersListComponentProps { + paginationProps: PaginationProps; + usersResponse: getUsersResponse; + handleDeleteUserButtonClick: (id: number) => void; +} + +const UsersListComponent = ({ + paginationProps, + usersResponse, + handleDeleteUserButtonClick, +}: UsersListComponentProps) => { + return ( + + + + + {usersResponse.status === 200 ? ( + + ) : ( + + )} + + + + + ); +}; + +export default UsersListComponent; diff --git a/frontend/app/components/users/UsersListTable.tsx b/frontend/app/components/users/UsersListTable.tsx new file mode 100644 index 00000000..15d514ec --- /dev/null +++ b/frontend/app/components/users/UsersListTable.tsx @@ -0,0 +1,39 @@ +import { rem, Table } from '@mantine/core'; +import { GetUsers200UsersItem } from 'client/client.schemas'; +import UserDeleteButton from './UserDeleteButton'; +import NoUserComponent from './NoUserComponent'; + +interface UsersTableProps { + users: GetUsers200UsersItem[]; + handleDeleteUserButtonClick: (id: number) => void; +} + +const UsersListTable = ({ + users, + handleDeleteUserButtonClick, +}: UsersTableProps) => { + if (users.length === 0) return ; + return ( + + + {users.map((user) => ( + + {user.id && ( + <> + {user.name} + + + + + )} + + ))} + +
+ ); +}; + +export default UsersListTable; diff --git a/frontend/app/components/users/UsersListTitle.tsx b/frontend/app/components/users/UsersListTitle.tsx new file mode 100644 index 00000000..29debb02 --- /dev/null +++ b/frontend/app/components/users/UsersListTitle.tsx @@ -0,0 +1,15 @@ +import { Center, Group, Title } from '@mantine/core'; +import { FaUsers } from 'react-icons/fa'; + +const UsersListTitle = () => { + return ( +
+ + + ユーザー一覧 + +
+ ); +}; + +export default UsersListTitle; diff --git a/frontend/app/routes/home._index/route.tsx b/frontend/app/routes/home._index/route.tsx index d2a7a4f5..a2b4a8bd 100644 --- a/frontend/app/routes/home._index/route.tsx +++ b/frontend/app/routes/home._index/route.tsx @@ -13,6 +13,7 @@ import { useEffect } from 'react'; import BookListComponent from '~/components/books/BookListComponent'; import { commitSession, getSession } from '~/services/session.server'; import { SelectedBookProps, selectedBooksAtom } from '~/stores/bookAtom'; +import { ActionResponse } from '~/types/response'; interface LoaderData { booksResponse: getBooksResponse; @@ -26,11 +27,6 @@ interface LoaderData { }; } -export interface ActionResponse { - method: string; - status: number; -} - export const loader = async ({ request }: LoaderFunctionArgs) => { // 検索条件を取得する const url = new URL(request.url); @@ -263,11 +259,13 @@ const BooKListPage = () => { isOpen={opened} open={open} close={close} - handlePaginationChange={handlePaginationChange} - handleLimitChange={handleLimitChange} - page={page ? Number(page) : undefined} - limit={limit ? Number(limit) : undefined} - totalBook={booksResponse.data.totalBook} + paginationProps={{ + handlePaginationChange: handlePaginationChange, + handleLimitChange: handleLimitChange, + page: page ? Number(page) : undefined, + limit: limit ? Number(limit) : undefined, + total: booksResponse.data.totalBook, + }} /> ); }; diff --git a/frontend/app/routes/home.books.$bookId/route.tsx b/frontend/app/routes/home.books.$bookId/route.tsx index 28f776f3..d80df2f6 100644 --- a/frontend/app/routes/home.books.$bookId/route.tsx +++ b/frontend/app/routes/home.books.$bookId/route.tsx @@ -12,17 +12,13 @@ import { commitSession, getSession } from '~/services/session.server'; import { Book } from 'client/client.schemas'; import BookDetailControlPanel from '~/components/book-detail/BookDetailControlPanel'; import ErrorComponent from '~/components/common/error/ErrorComponent'; +import { ActionResponse } from '~/types/response'; interface LoaderData { bookResponse: getBookResponse; loansResponse?: getLoansResponse; } -interface ActionResponse { - method: string; - status: number; -} - export interface BookDetailOutletContext { book: Book; loansResponse?: getLoansResponse; diff --git a/frontend/app/routes/home.global._index/route.tsx b/frontend/app/routes/home.global._index/route.tsx index 4c54b2c8..eea8ba11 100644 --- a/frontend/app/routes/home.global._index/route.tsx +++ b/frontend/app/routes/home.global._index/route.tsx @@ -270,7 +270,7 @@ const GlobalBookListPage = () => { handleLimitChange: handleLimitChange, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined, - totalBook: booksResponse ? booksResponse.data.totalBook : 0, + total: booksResponse ? booksResponse.data.totalBook : 0, }} searchMode={searchMode} setSearchMode={setSearchMode} diff --git a/frontend/app/routes/home.global.books.$isbn/route.tsx b/frontend/app/routes/home.global.books.$isbn/route.tsx index 1331d39a..ecb68550 100644 --- a/frontend/app/routes/home.global.books.$isbn/route.tsx +++ b/frontend/app/routes/home.global.books.$isbn/route.tsx @@ -16,7 +16,7 @@ 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'; +import { ActionResponse } from '~/types/response'; interface LoaderData { searchBooksResponse: searchBooksResponse; @@ -29,7 +29,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { // 書籍の情報を取得する const isbn = params.isbn ?? ''; const searchBooksResponse = await searchBooks({ isbn: isbn }); - // 既に登録済みであるか確認するため + // 既に登録済みであるか確認するため if (session.has('user')) { const getBookResponse = await getBooks({ isbn: isbn }); if (getBookResponse.data.totalBook > 0) { diff --git a/frontend/app/routes/home.users._index/route.tsx b/frontend/app/routes/home.users._index/route.tsx new file mode 100644 index 00000000..f873551d --- /dev/null +++ b/frontend/app/routes/home.users._index/route.tsx @@ -0,0 +1,145 @@ +import { + ActionFunctionArgs, + json, + LoaderFunctionArgs, + redirect, +} from '@remix-run/cloudflare'; +import { useFetcher, useLoaderData, useNavigate } from '@remix-run/react'; +import { deleteUser, getUsers, getUsersResponse } from 'client/client'; +import UsersListComponent from '~/components/users/UsersListComponent'; +import { commitSession, getSession } from '~/services/session.server'; +import { ActionResponse } from '~/types/response'; + +interface LoaderData { + usersResponse: getUsersResponse; + condition: { + page?: string; + limit?: string; + }; +} + +export const loader = async ({ request }: LoaderFunctionArgs) => { + // 検索条件を取得する + const url = new URL(request.url); + const page = url.searchParams.get('page') ?? undefined; + const limit = url.searchParams.get('limit') ?? undefined; + // ユーザー情報を取得する + const response = await getUsers({ page: page, limit: limit }); + + return json({ + usersResponse: response, + condition: { + page: page, + limit: limit, + }, + }); +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + const session = await getSession(request.headers.get('Cookie')); + const formData = await request.formData(); + const userId = String(formData.get('userId')); + + // 未ログインの場合 + if (!session.has('user')) { + session.flash('error', 'ログインしてください'); + return redirect('/login', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + } + + const cookieHeader = [ + `__Secure-user_id=${session.get('user')?.id};`, + `__Secure-session_token=${session.get('user')?.sessionToken}`, + ].join('; '); + + const response = await deleteUser(userId, { + headers: { Cookie: cookieHeader }, + }); + + switch (response.status) { + case 204: + session.flash('success', 'ユーザーを削除しました'); + if (Number(userId) === session.get('user')?.id) { + session.unset('user'); + session.flash('success', 'ログアウトしました'); + return redirect('/home', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + } + break; + case 401: + session.flash('error', 'ログインしてください'); + return redirect('/login', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + case 404: + session.flash('error', 'ユーザーが見つかりませんでした'); + break; + case 500: + session.flash('error', 'サーバーエラーが発生しました'); + break; + } + return json( + { method: 'DELETE', status: response.status }, + { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }, + ); +}; + +const UsersListPage = () => { + const { usersResponse, condition } = useLoaderData(); + const { page, limit } = condition; + const navigate = useNavigate(); + const fetcher = useFetcher(); + const handlePaginationChange = (newPage: number) => { + let url = '/home/users'; + let initial = true; + if (limit) { + url = + initial === true ? `${url}?limit=${limit}` : `${url}&limit=${limit}`; + initial = false; + } + url = + initial === true ? `${url}?page=${newPage}` : `${url}&page=${newPage}`; + navigate(url); + }; + + const handleLimitChange = (newLimit: number) => { + navigate(`/home/users?limit=${newLimit}`); + }; + + const handleDeleteUserButtonClick = (id: number) => { + fetcher.submit( + { userId: id }, + { + method: 'DELETE', + }, + ); + }; + + return ( + + ); +}; + +export default UsersListPage; diff --git a/frontend/app/routes/home.users/route.tsx b/frontend/app/routes/home.users/route.tsx new file mode 100644 index 00000000..6e404908 --- /dev/null +++ b/frontend/app/routes/home.users/route.tsx @@ -0,0 +1,22 @@ +import { LoaderFunctionArgs, redirect } from '@remix-run/cloudflare'; +import { Outlet } from '@remix-run/react'; +import { commitSession, getSession } from '~/services/session.server'; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const session = await getSession(request.headers.get('Cookie')); + + if (!session.has('user')) { + return redirect('/login', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); + } + return null; +}; + +const UsersLayout = () => { + return ; +}; + +export default UsersLayout; diff --git a/frontend/app/routes/home/route.tsx b/frontend/app/routes/home/route.tsx index 467d645a..461210f6 100644 --- a/frontend/app/routes/home/route.tsx +++ b/frontend/app/routes/home/route.tsx @@ -101,7 +101,7 @@ const Home = () => { useEffect(() => { if (navigation.state === 'idle') { - if (!!userData) { + if (userData) { // Cookieにユーザ情報が保存されている if (!user) { // 状態変数にユーザ情報を保存する diff --git a/frontend/app/types/modal.d.ts b/frontend/app/types/modal.d.ts new file mode 100644 index 00000000..86774161 --- /dev/null +++ b/frontend/app/types/modal.d.ts @@ -0,0 +1,5 @@ +export interface HandleModalProps { + isOpen: boolean; + open: () => void; + close: () => void; +} diff --git a/frontend/app/types/paginatiion.d.ts b/frontend/app/types/paginatiion.d.ts new file mode 100644 index 00000000..30987fd0 --- /dev/null +++ b/frontend/app/types/paginatiion.d.ts @@ -0,0 +1,7 @@ +export interface PaginationProps { + handlePaginationChange: (newPage: number) => void; + handleLimitChange: (newLimit: number) => void; + page?: number; + limit?: number; + total: number; +} diff --git a/frontend/app/types/response.d.ts b/frontend/app/types/response.d.ts new file mode 100644 index 00000000..87520f16 --- /dev/null +++ b/frontend/app/types/response.d.ts @@ -0,0 +1,4 @@ +export interface ActionResponse { + method: string; + status: number; +}