From d9a825ba7f910f8cb389cc4b6785d3ff17a81cac Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 9 Aug 2026 17:35:44 +0900 Subject: [PATCH 1/3] feat(favorites): add FavoriteHeartButton UI component - Presentational only: isFavorited/isLoading/onClick/className props - Uses MUI Favorite/FavoriteBorder icons via the shared IconButton wrapper, matching existing icon-button usage (e.g. Lightbox.tsx) - Stops click propagation internally so it's safe to place inside clickable cards --- .../components/FavoriteHeartButton.tsx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 client/src/app/features/favorites/components/FavoriteHeartButton.tsx diff --git a/client/src/app/features/favorites/components/FavoriteHeartButton.tsx b/client/src/app/features/favorites/components/FavoriteHeartButton.tsx new file mode 100644 index 0000000..3c849fb --- /dev/null +++ b/client/src/app/features/favorites/components/FavoriteHeartButton.tsx @@ -0,0 +1,33 @@ +import FavoriteIcon from "@mui/icons-material/Favorite"; +import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder"; +import IconButton from "@shared/uis/Icon-Button.tsx"; + +export function FavoriteHeartButton({ + isFavorited, + isLoading = false, + onClick, + className, +}: { + isFavorited: boolean; + isLoading?: boolean; + onClick: () => void; + className?: string; +}) { + const handleClick: React.MouseEventHandler = (event) => { + event.preventDefault(); + event.stopPropagation(); + onClick(); + }; + + return ( + + {isFavorited ? : } + + ); +} From d16c189f1c37933173309228f46421f2a55c59bd Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 9 Aug 2026 17:35:51 +0900 Subject: [PATCH 2/3] feat(favorites): wire FavoriteHeartButton into FavoriteButtonContainer - Replaces the plain-text placeholder button with the real UI component - FavoriteButtonContainer now accepts an optional className, passed through to style the icon per context (e.g. white on the card's dark image overlay) --- .../containers/favorite-button-container.tsx | 30 +++++++++---------- .../app/features/top/cards/HeritageCard.tsx | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/client/src/app/features/favorites/containers/favorite-button-container.tsx b/client/src/app/features/favorites/containers/favorite-button-container.tsx index f3a9472..f2d6e10 100644 --- a/client/src/app/features/favorites/containers/favorite-button-container.tsx +++ b/client/src/app/features/favorites/containers/favorite-button-container.tsx @@ -1,27 +1,25 @@ import { useAddFavorite } from "../hooks/use-add-favorite"; import { useAuth } from "@shared/auth/AuthHooks.ts"; +import { FavoriteHeartButton } from "../components/FavoriteHeartButton"; -export function FavoriteButtonContainer({ heritageId }: { heritageId: number }) { +export function FavoriteButtonContainer({ + heritageId, + className, +}: { + heritageId: number; + className?: string; +}) { const { user } = useAuth(); const { submit, isAdded, isLoading } = useAddFavorite(); if (!user) return null; - const handleClick = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - void submit(heritageId); - }; - return ( - + void submit(heritageId)} + className={className} + /> ); } diff --git a/client/src/app/features/top/cards/HeritageCard.tsx b/client/src/app/features/top/cards/HeritageCard.tsx index e1e5208..2175ea6 100644 --- a/client/src/app/features/top/cards/HeritageCard.tsx +++ b/client/src/app/features/top/cards/HeritageCard.tsx @@ -55,7 +55,7 @@ export function HeritageCard({
- +
{item.isEndangered && ( From 6e052e319938b56dccf35b207c9675e82c760f6d Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 9 Aug 2026 17:35:58 +0900 Subject: [PATCH 3/3] test(favorites): add FavoriteHeartButton tests --- .../__tests__/FavoriteHeartButton.test.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 client/src/app/features/favorites/components/__tests__/FavoriteHeartButton.test.tsx diff --git a/client/src/app/features/favorites/components/__tests__/FavoriteHeartButton.test.tsx b/client/src/app/features/favorites/components/__tests__/FavoriteHeartButton.test.tsx new file mode 100644 index 0000000..cb7689c --- /dev/null +++ b/client/src/app/features/favorites/components/__tests__/FavoriteHeartButton.test.tsx @@ -0,0 +1,47 @@ +/** @jest-environment jsdom */ + +import "@testing-library/jest-dom/jest-globals"; +import { describe, test, expect, jest } from "@jest/globals"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { FavoriteHeartButton } from "../FavoriteHeartButton"; + +describe("FavoriteHeartButton", () => { + test("未登録状態: 'Add to favorites'ラベルで表示され、クリックでonClickが呼ばれる", () => { + const onClick = jest.fn(); + render(); + + const button = screen.getByRole("button", { name: "Add to favorites" }); + expect(button).not.toBeDisabled(); + + fireEvent.click(button); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + test("登録済み状態: 'Favorited'ラベルで表示される", () => { + render(); + + expect(screen.getByRole("button", { name: "Favorited" })).toBeInTheDocument(); + }); + + test("isLoading=trueの場合はボタンが無効化される", () => { + render(); + + expect(screen.getByRole("button", { name: "Add to favorites" })).toBeDisabled(); + }); + + test("クリックイベントの伝播を止める(カードのクリックに巻き込まれない)", () => { + const onClick = jest.fn(); + const onCardClick = jest.fn(); + + render( +
+ +
, + ); + + fireEvent.click(screen.getByRole("button", { name: "Add to favorites" })); + + expect(onClick).toHaveBeenCalledTimes(1); + expect(onCardClick).not.toHaveBeenCalled(); + }); +});