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 (
+ }
+ component="a"
+ href="users/add"
+ >
+ ユーザー追加
+
+ );
+};
+
+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;
+}