👔 feat: add optimistic update for bookmark button
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 52s

This commit is contained in:
2026-03-31 21:25:20 +07:00
parent 99bf72c1af
commit 2c0ece7870
7 changed files with 76 additions and 17 deletions

View File

@ -0,0 +1,21 @@
"use server";
import { backendFetch } from "@/shared/helpers/backendFetch";
export const removeHeroBannerMediaFromSaved = async (mediaId: string) => {
try {
return await backendFetch("collections/sys", {
method: "DELETE",
body: JSON.stringify({
name: "Saved",
itemId: mediaId,
}),
});
} catch (error) {
console.error("Error removing media from saved list:", error);
return {
success: false,
message: "Failed to remove media from saved list.",
};
}
};

View File

@ -1,29 +1,63 @@
"use client"; "use client";
import { addHeroBannerMediaToSaved } from "@/features/home/actions/Hero/addHeroBannerMediaToSaved"; import { addHeroBannerMediaToSaved } from "@/features/home/actions/Hero/addHeroBannerMediaToSaved";
import { removeHeroBannerMediaFromSaved } from "@/features/home/actions/Hero/removeHeroBannerMediaFromSaved";
import { useAuth } from "@/shared/contexts/AuthContext"; import { useAuth } from "@/shared/contexts/AuthContext";
import { BackendResponse } from "@/shared/helpers/backendFetch";
import { Button } from "@/shared/libs/shadcn/ui/button"; import { Button } from "@/shared/libs/shadcn/ui/button";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import React from "react";
const AddToList = ({ mediaId }: { mediaId: string }) => { const AddToList = ({
mediaId,
isInCollection,
}: {
mediaId: string;
isInCollection: boolean;
}) => {
const { session } = useAuth(); const { session } = useAuth();
const [isSaved, setIsSaved] = React.useState<boolean>(isInCollection);
const handleAddToList = async () => { const handleAddToList = async () => {
const result = await addHeroBannerMediaToSaved(mediaId); setIsSaved(!isSaved);
console.log("Hasil dari fungsi server:", result); const result = (await addHeroBannerMediaToSaved(mediaId).catch(
(_) => void _,
)) as BackendResponse<undefined>;
if (!result || !result.success) {
setIsSaved((prev) => !prev);
}
};
const handleRemoveFromList = async () => {
setIsSaved(!isSaved);
const result = (await removeHeroBannerMediaFromSaved(mediaId).catch(
(_) => void _,
)) as BackendResponse<undefined>;
if (!result || !result.success) {
setIsSaved((prev) => !prev);
}
}; };
return ( return (
<div> <div>
{session?.user && ( {session?.user &&
<Button (isSaved ? (
onClick={handleAddToList} <Button
variant="secondary" onClick={handleRemoveFromList}
className="h-full flex gap-1 px-4 rounded-xl border border-neutral-400/10 bg-neutral-950/20 hover:bg-neutral-950/40 backdrop-blur-lg text-neutral-200" variant="secondary"
> className="h-full flex gap-1 px-4 rounded-xl border border-neutral-400/10 bg-neutral-950/20 hover:bg-neutral-950/40 backdrop-blur-lg text-neutral-200"
<Icon icon="boxicons:bookmark" className="size-5.5" /> >
<span>Add to List</span> <Icon icon="boxicons:bookmark-filled" className="size-5.5" />
</Button> <span>Remove from List</span>
)} </Button>
) : (
<Button
onClick={handleAddToList}
variant="secondary"
className="h-full flex gap-1 px-4 rounded-xl border border-neutral-400/10 bg-neutral-950/20 hover:bg-neutral-950/40 backdrop-blur-lg text-neutral-200"
>
<Icon icon="boxicons:bookmark" className="size-5.5" />
<span>Add to List</span>
</Button>
))}
</div> </div>
); );
}; };

View File

@ -19,6 +19,7 @@ export interface HeroSwiperProps {
slug: string; slug: string;
name: string; name: string;
}[]; }[];
isInCollection: boolean;
}[]; }[];
} }
@ -81,7 +82,10 @@ const HeroSwiper = (props: HeroSwiperProps) => {
</span> </span>
</Button> </Button>
</Link> </Link>
<AddToList mediaId={slide.id} /> <AddToList
mediaId={slide.id}
isInCollection={slide.isInCollection}
/>
</div> </div>
</div> </div>
</SwiperSlide> </SwiperSlide>

View File

@ -1,4 +1,4 @@
import { RecommendationAnime } from "@/features/home/actions/getRecommenationAnime"; import { RecommendationAnime } from "@/features/home/actions/Hero/getRecommenationAnime";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
const AnimeRecommendationCard = ({ data }: { data: RecommendationAnime }) => { const AnimeRecommendationCard = ({ data }: { data: RecommendationAnime }) => {

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useRef } from "react"; import { useRef } from "react";
import { RecommendationAnime } from "../../actions/getRecommenationAnime"; import { RecommendationAnime } from "../../actions/Hero/getRecommenationAnime";
import AnimeRecommendationCard from "./components/Card"; import AnimeRecommendationCard from "./components/Card";
import ScrollingButton from "./components/ScrollingButton"; import ScrollingButton from "./components/ScrollingButton";

View File

@ -1,4 +1,4 @@
import { getRecommendationAnimeAction } from "../../actions/getRecommenationAnime"; import { getRecommendationAnimeAction } from "../../actions/Hero/getRecommenationAnime";
import RecommendationClient from "./main.client"; import RecommendationClient from "./main.client";
const RecommendationMain = async () => { const RecommendationMain = async () => {