fix: button save and category in page for-you

This commit is contained in:
Sabda Yagra 2025-10-20 22:22:35 +07:00
parent ef4887cfc1
commit 28c1b79812
2 changed files with 211 additions and 128 deletions

View File

@ -14,9 +14,6 @@ import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
export default function Header() { export default function Header() {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set()); const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [userId, setUserId] = useState<string | null>(
getCookiesDecrypt("uie") || null
);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
@ -29,7 +26,7 @@ export default function Header() {
const response = await listArticles( const response = await listArticles(
1, 1,
5, 5,
1, // ⬅️ tambahkan typeId = 1 (image only) 1, // hanya typeId = 1 (image)
undefined, undefined,
undefined, undefined,
"createdAt", "createdAt",
@ -39,7 +36,6 @@ export default function Header() {
let articlesData: any[] = []; let articlesData: any[] = [];
if (response?.error) { if (response?.error) {
// Jika gagal, fallback tetap filter typeId = 1
const fallback = await listData( const fallback = await listData(
"", "",
"", "",
@ -49,7 +45,7 @@ export default function Header() {
"createdAt", "createdAt",
"", "",
"", "",
"1" // ⬅️ tambahkan filter typeId = 1 di fallback juga "1"
); );
articlesData = fallback?.data?.data?.content || []; articlesData = fallback?.data?.data?.content || [];
} else { } else {
@ -69,39 +65,50 @@ export default function Header() {
fetchData(); fetchData();
}, [slug]); }, [slug]);
// ✅ Sinkronisasi bookmark: dari localStorage + backend user login
useEffect(() => { useEffect(() => {
const fetchBookmarks = async () => { const syncBookmarks = async () => {
const currentUserId: any = getCookiesDecrypt("uie");
setUserId(currentUserId);
if (!currentUserId) {
setBookmarkedIds(new Set());
return;
}
try { try {
const res = await getBookmarkSummaryForUser(); const roleId = Number(getCookiesDecrypt("urie"));
const bookmarks = let localSet = new Set<number>();
res?.data?.data?.recentBookmarks ||
res?.data?.data?.bookmarks ||
res?.data?.data ||
[];
const ids = new Set<number>( const simpananLocal = localStorage.getItem("bookmarkedIds");
(Array.isArray(bookmarks) ? bookmarks : []) if (simpananLocal) {
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id)) localSet = new Set(JSON.parse(simpananLocal));
.filter((x) => !isNaN(x)) }
);
setBookmarkedIds(ids); // Jika user login, gabungkan dengan data dari backend
if (roleId && !isNaN(roleId)) {
const res = await getBookmarkSummaryForUser();
const bookmarks =
res?.data?.data?.recentBookmarks ||
res?.data?.data?.bookmarks ||
res?.data?.data ||
[];
const ids = new Set<number>(
(Array.isArray(bookmarks) ? bookmarks : [])
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
.filter((x) => !isNaN(x))
);
const gabungan = new Set([...localSet, ...ids]);
setBookmarkedIds(gabungan);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(gabungan))
);
} else {
// Jika belum login, pakai local saja
setBookmarkedIds(localSet);
}
} catch (err) { } catch (err) {
console.error("Gagal memuat bookmark user:", err); console.error("Gagal sinkronisasi bookmark:", err);
setBookmarkedIds(new Set());
} }
}; };
fetchBookmarks(); syncBookmarks();
}, [userId]); }, []);
return ( return (
<section className="max-w-[1350px] mx-auto px-4"> <section className="max-w-[1350px] mx-auto px-4">
@ -111,17 +118,8 @@ export default function Header() {
key={data[0].id} key={data[0].id}
item={data[0]} item={data[0]}
isBig isBig
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))} bookmarkedIds={bookmarkedIds}
onSaved={(id) => setBookmarkedIds={setBookmarkedIds}
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
onRemoved={(id) =>
setBookmarkedIds((prev) => {
const next = new Set(prev);
next.delete(Number(id));
return next;
})
}
/> />
)} )}
@ -130,17 +128,8 @@ export default function Header() {
<Card <Card
key={item.id} key={item.id}
item={item} item={item}
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))} bookmarkedIds={bookmarkedIds}
onSaved={(id) => setBookmarkedIds={setBookmarkedIds}
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
onRemoved={(id) =>
setBookmarkedIds((prev) => {
const next = new Set(prev);
next.delete(Number(id));
return next;
})
}
/> />
))} ))}
</div> </div>
@ -158,7 +147,7 @@ export default function Header() {
); );
} }
// 🔹 Helper function untuk transform data // 🔹 Helper function
function itemTransform(article: any) { function itemTransform(article: any) {
return { return {
id: article.id, id: article.id,
@ -185,29 +174,25 @@ function itemTransform(article: any) {
}; };
} }
// 🔹 Komponen Card
function Card({ function Card({
item, item,
isBig = false, isBig = false,
isInitiallyBookmarked = false, bookmarkedIds,
onSaved, setBookmarkedIds,
onRemoved,
}: { }: {
item: any; item: any;
isBig?: boolean; isBig?: boolean;
isInitiallyBookmarked?: boolean; bookmarkedIds: Set<number>;
onSaved?: (id: number) => void; setBookmarkedIds: React.Dispatch<React.SetStateAction<Set<number>>>;
onRemoved?: (id: number) => void;
}) { }) {
const router = useRouter(); const router = useRouter();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
useEffect(() => { const isBookmarked = bookmarkedIds.has(Number(item.id));
setIsBookmarked(isInitiallyBookmarked);
}, [isInitiallyBookmarked]);
const getLink = () => `/content/image/detail/${item?.id}`; const getLink = () => `/content/image/detail/${item?.id}`;
const handleToggleBookmark = async () => { const handleToggleBookmark = async () => {
const roleId = Number(getCookiesDecrypt("urie")); const roleId = Number(getCookiesDecrypt("urie"));
@ -234,18 +219,27 @@ function Card({
confirmButtonColor: "#d33", confirmButtonColor: "#d33",
}); });
} else { } else {
const nowBookmarked = !isBookmarked; const updated = new Set(bookmarkedIds);
setIsBookmarked(nowBookmarked); let pesan = "";
if (nowBookmarked) onSaved?.(item.id);
else onRemoved?.(item.id); if (isBookmarked) {
updated.delete(Number(item.id));
pesan = "Dihapus dari bookmark.";
} else {
updated.add(Number(item.id));
pesan = "Artikel disimpan ke bookmark.";
}
setBookmarkedIds(updated);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(updated))
);
MySwal.fire({ MySwal.fire({
icon: "success", icon: "success",
title: nowBookmarked ? "Disimpan!" : "Dihapus!", title: isBookmarked ? "Dihapus!" : "Disimpan!",
text: nowBookmarked text: pesan,
? "Artikel berhasil disimpan ke bookmark."
: "Artikel dihapus dari bookmark.",
confirmButtonColor: "#3085d6",
timer: 1500, timer: 1500,
showConfirmButton: false, showConfirmButton: false,
}); });
@ -264,30 +258,26 @@ function Card({
}; };
return ( return (
<div> <div
<div className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${ isBig
isBig ? "w-full lg:max-w-[670px] h-[680px]"
? "w-full lg:max-w-[670px] lg:min-h-[680px]" : "w-full h-[360px] md:h-[340px]"
: "w-full h-[350px] md:h-[330px]" }`}
}`} >
> <div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
<div <Link href={getLink()}>
className={`relative ${ <Image
isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video" src={item.smallThumbnailLink || "/contributor.png"}
} w-full`} alt={item.title}
> fill
<Link href={getLink()}> className="object-cover"
<Image />
src={item.smallThumbnailLink || "/contributor.png"} </Link>
alt={item.title} </div>
fill
className="object-cover"
/>
</Link>
</div>
<div className="p-6 space-y-2"> <div className="p-4 flex flex-col justify-between flex-1">
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap"> <div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded"> <span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
{item.clientName} {item.clientName}
@ -317,31 +307,31 @@ function Card({
{item.title} {item.title}
</h3> </h3>
</Link> </Link>
</div>
<div className="flex justify-between items-center pt-4"> <div className="flex justify-between items-center pt-4">
<div className="flex gap-2 text-gray-500"> <div className="flex gap-2 text-gray-500">
<ThumbsUp <ThumbsUp
size={18} size={18}
className="cursor-pointer hover:text-[#F60100]" className="cursor-pointer hover:text-[#F60100]"
/> />
<ThumbsDown <ThumbsDown
size={18} size={18}
className="cursor-pointer hover:text-red-600" className="cursor-pointer hover:text-red-600"
/> />
</div>
<button
onClick={handleToggleBookmark}
disabled={isSaving}
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
isBookmarked
? "bg-gray-400 text-white hover:bg-gray-500"
: "bg-[#F60100] text-white hover:bg-[#c90000]"
}`}
>
{isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
</button>
</div> </div>
<button
onClick={handleToggleBookmark}
disabled={isSaving || isBookmarked}
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
isBookmarked
? "bg-gray-400 text-white cursor-not-allowed"
: "bg-[#F60100] text-white hover:bg-[#c90000]"
}`}
>
{isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,10 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getPublicClients, PublicClient } from "@/service/client/public-clients"; import {
getPublicClients,
PublicClient,
} from "@/service/client/public-clients";
type CategoryTabsProps = { type CategoryTabsProps = {
selectedCategory: string; selectedCategory: string;
@ -15,17 +18,21 @@ export default function CategoryTabs({
const [clients, setClients] = useState<PublicClient[]>([]); const [clients, setClients] = useState<PublicClient[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Fetch public clients // Fetch public clients dari API
useEffect(() => { useEffect(() => {
async function fetchClients() { async function fetchClients() {
try { try {
const response = await getPublicClients(); const response = await getPublicClients();
if (response?.data?.success && response.data.data) { if (response?.data?.success && response.data.data) {
setClients(response.data.data); // 🔹 Filter hanya client aktif (boolean true)
const activeClients = response.data.data.filter(
(client: PublicClient) => client.isActive === true
);
setClients(activeClients);
} }
} catch (error) { } catch (error) {
console.error("Error fetching public clients:", error); console.error("Error fetching public clients:", error);
// Fallback to empty array if API fails // Fallback ke data statis jika API gagal
setClients([]); setClients([]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -35,9 +42,22 @@ export default function CategoryTabs({
fetchClients(); fetchClients();
}, []); }, []);
// Create categories array with "SEMUA" first, then client names // 🔹 Fallback jika API gagal
const categories = ["SEMUA", ...clients.map(client => client.name)]; const fallbackClients = [
{ id: 1, name: "DIV HUMAS POLRI" },
{ id: 2, name: "POLDASUMUT" },
{ id: 3, name: "POLDAMETROJAYA" },
{ id: 4, name: "POLDARIYAD" },
{ id: 5, name: "POLDABALI" },
];
const displayClients =
clients.length > 0 ? clients : (fallbackClients as any);
// 🔹 Susun kategori: “SEMUA” di awal
const categories = ["SEMUA", ...displayClients.map((c: any) => c.name)];
// 🔹 Tampilkan skeleton saat loading
if (loading) { if (loading) {
return ( return (
<div className="flex flex-wrap gap-2 overflow-x-auto"> <div className="flex flex-wrap gap-2 overflow-x-auto">
@ -51,6 +71,7 @@ export default function CategoryTabs({
); );
} }
// 🔹 Render kategori client
return ( return (
<div className="flex flex-wrap gap-2 overflow-x-auto"> <div className="flex flex-wrap gap-2 overflow-x-auto">
{categories.map((cat, idx) => ( {categories.map((cat, idx) => (
@ -59,8 +80,8 @@ export default function CategoryTabs({
onClick={() => onCategoryChange(cat)} onClick={() => onCategoryChange(cat)}
className={`px-4 py-1 text-sm rounded font-medium border transition-colors ${ className={`px-4 py-1 text-sm rounded font-medium border transition-colors ${
selectedCategory === cat selectedCategory === cat
? "bg-[#C6A455] text-white" ? "bg-[#C6A455] text-white border-[#C6A455]"
: "bg-white text-gray-800 hover:bg-gray-50" : "bg-white text-gray-800 hover:bg-gray-50 border-gray-300"
}`} }`}
> >
{cat.toUpperCase()} {cat.toUpperCase()}
@ -69,3 +90,75 @@ export default function CategoryTabs({
</div> </div>
); );
} }
// "use client";
// import { useState, useEffect } from "react";
// import { getPublicClients, PublicClient } from "@/service/client/public-clients";
// type CategoryTabsProps = {
// selectedCategory: string;
// onCategoryChange: (category: string) => void;
// };
// export default function CategoryTabs({
// selectedCategory,
// onCategoryChange,
// }: CategoryTabsProps) {
// const [clients, setClients] = useState<PublicClient[]>([]);
// const [loading, setLoading] = useState(true);
// // Fetch public clients
// useEffect(() => {
// async function fetchClients() {
// try {
// const response = await getPublicClients();
// if (response?.data?.success && response.data.data) {
// setClients(response.data.data);
// }
// } catch (error) {
// console.error("Error fetching public clients:", error);
// // Fallback to empty array if API fails
// setClients([]);
// } finally {
// setLoading(false);
// }
// }
// fetchClients();
// }, []);
// // Create categories array with "SEMUA" first, then client names
// const categories = ["SEMUA", ...clients.map(client => client.name)];
// if (loading) {
// return (
// <div className="flex flex-wrap gap-2 overflow-x-auto">
// {Array.from({ length: 6 }).map((_, idx) => (
// <div
// key={idx}
// className="px-4 py-1 text-sm rounded font-medium border bg-gray-200 animate-pulse h-8 w-20"
// ></div>
// ))}
// </div>
// );
// }
// return (
// <div className="flex flex-wrap gap-2 overflow-x-auto">
// {categories.map((cat, idx) => (
// <button
// key={idx}
// onClick={() => onCategoryChange(cat)}
// className={`px-4 py-1 text-sm rounded font-medium border transition-colors ${
// selectedCategory === cat
// ? "bg-[#C6A455] text-white"
// : "bg-white text-gray-800 hover:bg-gray-50"
// }`}
// >
// {cat.toUpperCase()}
// </button>
// ))}
// </div>
// );
// }