fix: all detail content, page untuk anda

This commit is contained in:
Sabda Yagra 2025-10-16 00:03:41 +07:00
parent 74e2588aaf
commit 249f84c002
11 changed files with 1301 additions and 754 deletions

View File

@ -5,7 +5,13 @@ import AudioDetail from "@/components/main/content/audio-detail";
export default function DetailAudioInfo() {
const params = useParams();
const id = params?.id as string;
const idParam = params?.id;
const id =
typeof idParam === "string"
? Number(idParam)
: Array.isArray(idParam)
? Number(idParam[0])
: 0;
return <AudioDetail id={id} />;
}

View File

@ -5,7 +5,13 @@ import DocumentDetail from "@/components/main/content/document-detail";
export default function DetailDocumentInfo() {
const params = useParams();
const id = params?.id as string;
const idParam = params?.id;
const id =
typeof idParam === "string"
? Number(idParam)
: Array.isArray(idParam)
? Number(idParam[0])
: 0;
return <DocumentDetail id={id} />;
}

View File

@ -119,7 +119,7 @@ export default function Header() {
};
fetchBookmarks();
}, [userId]); // ← otomatis re-fetch kalau cookie user berubah
}, [userId]);
return (
<section className="max-w-[1350px] mx-auto px-4">

View File

@ -17,7 +17,6 @@ import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { ThumbsUp, ThumbsDown } from "lucide-react";
// 🔹 Fungsi format tanggal ke WIB
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
@ -49,7 +48,6 @@ export default function MediaUpdate() {
const params = useParams();
const MySwal = withReactContent(Swal);
// Get slug from URL params
const slug = params?.slug as string;
useEffect(() => {
@ -73,15 +71,15 @@ export default function MediaUpdate() {
function getTypeIdByContentType(contentType: string): string {
switch (contentType) {
case "audiovisual":
return "2"; // Video
return "2";
case "foto":
return "1"; // Image
return "1";
case "audio":
return "4"; // Audio
return "4";
case "text":
return "3"; // Text
return "3";
default:
return "1"; // Default to Image
return "1";
}
}
@ -103,20 +101,41 @@ export default function MediaUpdate() {
// Function to get content type link for "Lihat lebih banyak" button
function getContentTypeLink() {
const pathname =
typeof window !== "undefined" ? window.location.pathname : "";
const isTenantRoute = pathname.includes("/tenant/");
const basePath = isTenantRoute ? `/tenant/${slug}/content` : "/content";
switch (contentType) {
case "audio":
return "/tenant/" + slug + "/content/audio";
return `${basePath}/audio`;
case "foto":
return "/tenant/" + slug + "/content/image";
return `${basePath}/image`;
case "audiovisual":
return "/tenant/" + slug + "/content/video";
return `${basePath}/video`;
case "text":
return "/tenant/" + slug + "/content/text";
return `${basePath}/text`;
default:
return "/tenant/" + slug + "/content/image"; // Default to image page
return `${basePath}/image`;
}
}
// function getContentTypeLink() {
// switch (contentType) {
// case "audio":
// return "/tenant/" + slug + "/content/audio";
// case "foto":
// return "/tenant/" + slug + "/content/image";
// case "audiovisual":
// return "/tenant/" + slug + "/content/video";
// case "text":
// return "/tenant/" + slug + "/content/text";
// default:
// return "/tenant/" + slug + "/content/image";
// }
// }
// if (contentType === "all") {
// setFilteredData(dataToRender);
// return;
@ -154,15 +173,15 @@ export default function MediaUpdate() {
const filtered = dataToRender.filter((item) => {
switch (contentType) {
case "audiovisual":
return item.typeId === 2; // Video
return item.typeId === 2;
case "audio":
return item.typeId === 4; // Audio
return item.typeId === 4;
case "foto":
return item.typeId === 1; // Image
return item.typeId === 1;
case "text":
return item.typeId === 3; // Text
return item.typeId === 3;
default:
return true; // Semua jenis
return true;
}
});
@ -173,11 +192,9 @@ export default function MediaUpdate() {
try {
setLoading(true);
// Tentukan typeId dari contentType
const typeId = parseInt(getTypeIdByContentType(contentType));
setCurrentTypeId(typeId.toString());
// 🔹 Ambil data artikel dari API
const response = await listArticles(
1,
10,
@ -204,11 +221,9 @@ export default function MediaUpdate() {
);
hasil = fallbackRes?.data?.data?.content || [];
} else {
// ✅ Perhatikan: API kamu mengembalikan `data.data` sebagai array
hasil = response?.data?.data || [];
}
// 🔹 Normalisasi struktur data
const transformedData = hasil.map((article: any) => ({
id: article.id,
title: article.title,
@ -224,10 +239,8 @@ export default function MediaUpdate() {
...article,
}));
// ✅ INI WAJIB: simpan hasil mapping ke state
setDataToRender(transformedData);
// 🔹 Sinkronisasi data bookmark
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
const simpananLocal = localStorage.getItem("bookmarkedIds");
@ -263,7 +276,6 @@ export default function MediaUpdate() {
}
}
// 🔹 Simpan bookmark
const handleSave = async (id: number) => {
const roleId = Number(getCookiesDecrypt("urie"));
if (!roleId || isNaN(roleId)) {

View File

@ -8,7 +8,7 @@ import { Button } from "../ui/button";
import Cookies from "js-cookie";
import { Card } from "../ui/card";
import { Input } from "../ui/input";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { Link } from "@/i18n/routing";
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
@ -28,86 +28,29 @@ const PUBLIKASI_SUBMENU = [
export default function Navbar() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
// const [user, setUser] = useState<{
// id: number;
// name: string;
// avatar: string;
// } | null>(null);
const [isDropdownOpen, setDropdownOpen] = useState(false);
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const pathname = usePathname();
const router = useRouter();
// useEffect(() => {
// const roleId = getCookiesDecrypt("urie");
// console.log("roleId", roleId);
// switch (roleId) {
// case "1":
// setUser({
// id: 1,
// name: "User Test",
// avatar: "/contributor.png",
// });
// break;
// case "3":
// setUser({
// id: 3,
// name: "Mabes Polri - Approver",
// avatar: "/contributor.png",
// });
// break;
// case "7":
// setUser({
// id: 7,
// name: "DivHumas - RoMulmed - BagDise",
// avatar: "/contributor.png",
// });
// break;
// case "6":
// setUser({
// id: 11,
// name: "jurnalis-kompas1",
// avatar: "/contributor.png",
// });
// break;
// default:
// setUser(null);
// }
// }, []);
// const roleId = getCookiesDecrypt("urie");
// const isLoggedIn = roleId !== null;
// 🔍 Fungsi untuk mengecek apakah user sedang login
// 🔍 Fungsi cek login
const checkLoginStatus = () => {
const roleId = getCookiesDecrypt("urie");
const fullname = Cookies.get("ufne");
return roleId && fullname ? true : false;
};
// 🔄 Cek status login saat pertama kali load
useEffect(() => {
setIsLoggedIn(checkLoginStatus());
}, []);
// const filteredNavItems = isLoggedIn
// ? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
// : NAV_ITEMS;
// 🔁 Gunakan isLoggedIn untuk menentukan navigasi
const filteredNavItems = isLoggedIn
? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
: NAV_ITEMS;
// 🚪 Fungsi logout
const handleLogout = () => {
// Hapus semua cookie dengan kemungkinan variasi path dan domain
Object.keys(Cookies.get()).forEach((cookieName) => {
Cookies.remove(cookieName, { path: "/" });
Cookies.remove(cookieName, {
@ -119,42 +62,13 @@ export default function Navbar() {
domain: window.location.hostname,
});
});
// Ubah state login jadi false agar tampilan langsung berubah
setIsLoggedIn(false);
setShowProfileMenu(false);
// Redirect ke homepage
window.location.href = "/";
};
// const handleLogout = () => {
// Object.keys(Cookies.get()).forEach((cookieName) => {
// Cookies.remove(cookieName);
// });
// window.location.href = "/";
// // setUser(null);
// setShowProfileMenu(false);
// };
const username = Cookies.get("username");
const fullname = Cookies.get("ufne");
// const router = useRouter();
// const [detail, setDetail] = useState<Detail>();
// const onLogout = () => {
// Object.keys(Cookies.get()).forEach((cookieName) => {
// Cookies.remove(cookieName);
// });
// router.push("/");
// };
const [isLogin, setIsLogin] = useState(false);
useEffect(() => {
setIsLogin(fullname ? true : false);
}, [fullname]);
return (
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
@ -166,7 +80,7 @@ export default function Navbar() {
fill
className="object-contain"
/>
</div>{" "}
</div>
<Menu
className="w-6 h-6 cursor-pointer"
onClick={() => setIsSidebarOpen(true)}
@ -174,10 +88,23 @@ export default function Navbar() {
<DynamicLogoTenant />
</div>
{/* 🌐 NAV MENU */}
<nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium">
{filteredNavItems.map((item) => {
const isActive = pathname === item.href;
// 🔹 Pengecekan khusus untuk "Untuk Anda"
const handleClick = (e: React.MouseEvent) => {
if (item.label === "Untuk Anda") {
e.preventDefault();
if (!checkLoginStatus()) {
router.push("/auth");
} else {
router.push("/in/public/for-you");
}
}
};
return (
<div key={item.label} className="relative">
{item.label === "Publikasi" ? (
@ -221,6 +148,7 @@ export default function Navbar() {
) : (
<Link
href={item.href}
onClick={handleClick}
className={cn(
"relative text-gray-500 hover:text-black transition-colors",
isActive && "text-black"
@ -236,6 +164,8 @@ export default function Navbar() {
);
})}
</nav>
{/* 🔹 PROFILE / LOGIN SECTION */}
<nav className="hidden md:flex items-center gap-3 z-10 relative">
{!isLoggedIn ? (
<>
@ -283,9 +213,10 @@ export default function Navbar() {
)}
</nav>
{/* 📱 SIDEBAR MOBILE */}
{isSidebarOpen && (
<div className="fixed inset-0 z-50 flex">
<div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto">
<div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto">
<button
onClick={() => setIsSidebarOpen(false)}
className="absolute top-4 right-4 text-gray-600"
@ -329,37 +260,39 @@ export default function Navbar() {
</h3>
<div className="space-y-5 ml-3">
{NAV_ITEMS.map((item) => (
<Link
<button
key={item.label}
href={item.href}
className="block text-[15px] text-gray-800"
onClick={() => setIsSidebarOpen(false)}
onClick={() => {
if (item.label === "Untuk Anda") {
if (!checkLoginStatus()) {
router.push("/auth");
} else {
router.push("/public/for-you");
}
} else {
router.push(item.href);
}
setIsSidebarOpen(false);
}}
className="block text-[15px] text-gray-800 text-left w-full"
>
{item.label}
</Link>
</button>
))}
</div>
</div>
<div className="space-y-5 text-[16px] font-bold">
<Link
href="/about"
className="block text-black text-[16px] font-bold"
>
<Link href="/about" className="block text-black">
Tentang Kami
</Link>
<Link
href="/advertising"
className="block text-[16px] font-bold text-black"
>
<Link href="/advertising" className="block text-black">
Advertising
</Link>
<Link
href="/contact"
className="block text-[16px] font-bold text-black"
>
<Link href="/contact" className="block text-black">
Kontak Kami
</Link>
{!isLoggedIn ? (
<>
<Link href="/auth" className="block text-lg text-gray-800">
@ -381,6 +314,7 @@ export default function Navbar() {
</button>
)}
</div>
<Card className="rounded-none p-4">
<h2 className="text-[#C6A455] text-center text-lg font-semibold">
Subscribe to Our Newsletter

View File

@ -1,9 +1,8 @@
"use client";
import Image from "next/image";
import { Calendar, Clock, Eye } from "lucide-react";
import { useState, useEffect } from "react";
import { Calendar, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useState, useEffect } from "react";
import {
FaFacebookF,
FaTiktok,
@ -15,31 +14,24 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import { getDetail } from "@/service/landing/landing";
import { getArticleDetail } from "@/service/content/content";
export default function AudioDetail({ id }: { id: string }) {
const [copied, setCopied] = useState(false);
const [showShareMenu, setShowShareMenu] = useState(false);
export default function AudioDetail({ id }: { id: number }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [selectedDoc, setSelectedDoc] = useState(0);
const [isLoading, setIsLoading] = useState<any>(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
const [copied, setCopied] = useState(false);
const [showShareMenu, setShowShareMenu] = useState(false);
const [selectedAudio, setSelectedAudio] = useState(0);
// Salin tautan
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy: ", err);
console.error("Gagal menyalin link:", err);
}
};
@ -60,86 +52,63 @@ export default function AudioDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
setLoading(true);
// Try new Articles API first
const response = await getArticleDetail(id);
console.log("Article Detail API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
// 1⃣ Coba API baru
const res = await getArticleDetail(id);
console.log("Audio detail API response:", res);
const article = res?.data?.data;
if (article) {
const mappedData = {
id: article.id,
title: article.title,
description: article.description,
createdAt: article.createdAt,
clickCount: article.viewCount,
creatorGroupLevelName: article.createdByName || "Unknown",
uploadedBy: {
publisher: article.createdByName || "MABES POLRI",
},
files:
article.files?.map((f: any) => ({
id: f.id,
fileName: f.file_name,
url: f.file_url,
secondaryUrl: f.file_url,
fileAlt: f.file_alt,
size: f.size,
createdAt: f.created_at,
updatedAt: f.updated_at,
})) || [],
};
setData(mappedData);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
secondaryUrl: file.file_url, // For audio files, use same URL
...file
})) || [],
...articleData
};
setData(transformedData);
console.log(
"doc",
transformedData.files[selectedDoc]?.secondaryUrl
);
}
} catch (error) {
console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
// 2⃣ Fallback ke API lama
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
} catch (err) {
console.error("Gagal memuat audio:", err);
// try {
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
// } catch (err2) {
// console.error("Fallback gagal:", err2);
// }
} finally {
setLoading(false);
}
};
if (id) fetchDetail();
}, [id]);
if (loading) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
<p>Loading...</p>
<p>Memuat data audio...</p>
</div>
);
}
@ -154,19 +123,25 @@ export default function AudioDetail({ id }: { id: string }) {
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Pemutar Audio Utama */}
<div className="relative">
<audio controls className="w-full" src={data?.files[selectedDoc]?.url}>
Your browser does not support the audio element.
<audio
controls
className="w-full"
src={data?.files?.[selectedAudio]?.url || ""}
>
Browser Anda tidak mendukung elemen audio.
</audio>
</div>
{/* Pilihan file audio */}
<div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
{data?.files?.map((file: any, index: number) => (
<button
key={file?.id}
onClick={() => setSelectedDoc(index)}
onClick={() => setSelectedAudio(index)}
className={`px-4 py-2 rounded-md border cursor-pointer hover:bg-gray-100 ${
selectedDoc === index ? "border-red-600 bg-gray-50" : ""
selectedAudio === index ? "border-red-600 bg-gray-50" : ""
}`}
>
🎵 {file?.fileName || `Audio ${index + 1}`}
@ -174,110 +149,45 @@ export default function AudioDetail({ id }: { id: string }) {
))}
</div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between text-sm text-muted-foreground">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
</span>
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
{new Date(data.createdAt)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(".", ":")}{" "}
WIB
</span>
{/* <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Clock className="w-4 h-4" />
{data.time || "-"}
</span> */}
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Eye className="w-4 h-4" />
{data.clickCount || 0}
</span>
<span className="text-black">
{" "}
Creator: {data.creatorGroupLevelName}
</span>
</div>
{/* Informasi artikel */}
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
</span>
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
{new Date(data.createdAt)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(".", ":")}{" "}
WIB
</span>
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Eye className="w-4 h-4" />
{data.clickCount || 0}
</span>
<span className="text-black">
Creator: {data.creatorGroupLevelName}
</span>
</div>
{/* Deskripsi */}
<div className="flex flex-col md:flex-row gap-6 mt-6">
{/* Sidebar actions */}
<div className="hidden md:flex flex-col gap-4 relative z-10">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
size="lg"
className="justify-start bg-black text-white rounded-full"
>
{copied ? <FaCheck /> : <FaLink />}
</Button>
<span>COPY LINK</span>
</div>
<div className="flex gap-2 items-center relative">
<Button
onClick={() => setShowShareMenu(!showShareMenu)}
size="lg"
className="justify-start bg-[#C6A455] text-white rounded-full"
>
<FaShareAlt />
</Button>
<span>SHARE</span>
{showShareMenu && (
<div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48">
<SocialItem icon={<FaFacebookF />} label="Facebook" />
<SocialItem icon={<FaTiktok />} label="TikTok" />
<SocialItem icon={<FaYoutube />} label="YouTube" />
<SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
<SocialItem icon={<FaInstagram />} label="Instagram" />
<SocialItem icon={<FaTwitter />} label="Twitter" />
</div>
)}
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
/>
</svg>
</Button>
COMMENT
</Link>
</div>
</div>
{/* Content */}
<div className="flex-1 space-y-4">
<h1 className="text-xl font-bold">{data.title}</h1>
<div className="text-base text-gray-700 leading-relaxed space-y-3">
<p>{data.description}</p>
</div>
{/* Actions bawah */}
<div className="flex flex-wrap md:flex-row justify-center gap-4 my-20">
{/* Tombol aksi bawah */}
<div className="flex flex-wrap justify-center gap-4 my-20">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
@ -288,7 +198,7 @@ export default function AudioDetail({ id }: { id: string }) {
</Button>
<span>COPY LINK</span>
</div>
<div className="flex gap-2 items-center">
<div className="flex gap-2 items-center relative">
<Button
onClick={() => setShowShareMenu(!showShareMenu)}
size="lg"
@ -297,25 +207,25 @@ export default function AudioDetail({ id }: { id: string }) {
<FaShareAlt />
</Button>
<span>SHARE</span>
{showShareMenu && (
<div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48 z-10">
<SocialItem icon={<FaFacebookF />} label="Facebook" />
<SocialItem icon={<FaTiktok />} label="TikTok" />
<SocialItem icon={<FaYoutube />} label="YouTube" />
<SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
<SocialItem icon={<FaInstagram />} label="Instagram" />
<SocialItem icon={<FaTwitter />} label="Twitter" />
</div>
)}
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/video/comment/${id}`}>
<Link href={`/content/audio/comment/${id}`}>
<Button
variant="default"
size="lg"
className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
/>
</svg>
💬
</Button>
COMMENT
</Link>
@ -326,3 +236,332 @@ export default function AudioDetail({ id }: { id: string }) {
</div>
);
}
// "use client";
// import Image from "next/image";
// import { Calendar, Clock, Eye } from "lucide-react";
// import { Button } from "@/components/ui/button";
// import Link from "next/link";
// import { useState, useEffect } from "react";
// import {
// FaFacebookF,
// FaTiktok,
// FaYoutube,
// FaWhatsapp,
// FaInstagram,
// FaTwitter,
// FaCheck,
// FaLink,
// FaShareAlt,
// } from "react-icons/fa";
// import { getDetail, getArticleDetail } from "@/service/landing/landing";
// export default function AudioDetail({ id }: { id: string }) {
// const [copied, setCopied] = useState(false);
// const [showShareMenu, setShowShareMenu] = useState(false);
// const [data, setData] = useState<any>(null);
// const [loading, setLoading] = useState(true);
// const [selectedDoc, setSelectedDoc] = useState(0);
// const [isLoading, setIsLoading] = useState<any>(true);
// useEffect(() => {
// const timer = setTimeout(() => {
// setIsLoading(false);
// }, 3000);
// return () => clearTimeout(timer);
// }, []);
// const handleCopyLink = async () => {
// try {
// await navigator.clipboard.writeText(window.location.href);
// setCopied(true);
// setTimeout(() => setCopied(false), 2000);
// } catch (err) {
// console.error("Failed to copy: ", err);
// }
// };
// const SocialItem = ({
// icon,
// label,
// }: {
// icon: React.ReactNode;
// label: string;
// }) => (
// <div className="flex items-center gap-3 cursor-pointer hover:opacity-80">
// <div className="bg-[#C6A455] p-2 rounded-full text-white">{icon}</div>
// <span className="text-sm">{label}</span>
// </div>
// );
// useEffect(() => {
// const fetchDetail = async () => {
// try {
// setLoading(true);
// // Try new Articles API first
// const response = await getArticleDetail(id);
// console.log("Article Detail API response:", response);
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
// console.log(
// "doc",
// fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
// );
// return;
// }
// // Handle new API response structure
// const articleData = response?.data?.data;
// if (articleData) {
// // Transform article data to match old structure for backward compatibility
// const transformedData = {
// id: articleData.id,
// title: articleData.title,
// description: articleData.description,
// createdAt: articleData.createdAt,
// clickCount: articleData.viewCount,
// creatorGroupLevelName: articleData.createdByName || "Unknown",
// uploadedBy: {
// publisher: articleData.createdByName || "MABES POLRI"
// },
// files: articleData.files?.map((file: any) => ({
// id: file.id,
// url: file.file_url,
// fileName: file.file_name,
// filePath: file.file_path,
// fileThumbnail: file.file_thumbnail,
// fileAlt: file.file_alt,
// widthPixel: file.width_pixel,
// heightPixel: file.height_pixel,
// size: file.size,
// downloadCount: file.download_count,
// createdAt: file.created_at,
// updatedAt: file.updated_at,
// secondaryUrl: file.file_url, // For audio files, use same URL
// ...file
// })) || [],
// ...articleData
// };
// setData(transformedData);
// console.log(
// "doc",
// transformedData.files[selectedDoc]?.secondaryUrl
// );
// }
// } catch (error) {
// console.error("Error fetching detail:", error);
// // Try fallback to old API if new API fails
// try {
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
// console.log(
// "doc",
// fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
// );
// } catch (fallbackError) {
// console.error("Fallback API also failed:", fallbackError);
// }
// } finally {
// setLoading(false);
// }
// };
// if (id) fetchDetail();
// }, [id]);
// if (loading) {
// return (
// <div className="max-w-6xl mx-auto px-4 py-6">
// <p>Loading...</p>
// </div>
// );
// }
// if (!data) {
// return (
// <div className="max-w-6xl mx-auto px-4 py-6">
// <p>Data tidak ditemukan</p>
// </div>
// );
// }
// return (
// <div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
// <div className="relative">
// <audio controls className="w-full" src={data?.files[selectedDoc]?.url}>
// Your browser does not support the audio element.
// </audio>
// </div>
// <div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
// {data?.files?.map((file: any, index: number) => (
// <button
// key={file?.id}
// onClick={() => setSelectedDoc(index)}
// className={`px-4 py-2 rounded-md border cursor-pointer hover:bg-gray-100 ${
// selectedDoc === index ? "border-red-600 bg-gray-50" : ""
// }`}
// >
// 🎵 {file?.fileName || `Audio ${index + 1}`}
// </button>
// ))}
// </div>
// <div className="flex flex-col md:flex-row md:items-center md:justify-between text-sm text-muted-foreground">
// <div className="flex flex-wrap items-center gap-2">
// <span className="font-semibold text-black border-r-2 pr-2 border-black">
// by {data?.uploadedBy?.publisher || "MABES POLRI"}
// </span>
// <span className="flex items-center gap-1 text-black">
// <Calendar className="w-4 h-4" />
// {new Date(data.createdAt)
// .toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// hour12: false,
// timeZone: "Asia/Jakarta",
// })
// .replace(".", ":")}{" "}
// WIB
// </span>
// {/* <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
// <Clock className="w-4 h-4" />
// {data.time || "-"}
// </span> */}
// <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
// <Eye className="w-4 h-4" />
// {data.clickCount || 0}
// </span>
// <span className="text-black">
// {" "}
// Creator: {data.creatorGroupLevelName}
// </span>
// </div>
// </div>
// <div className="flex flex-col md:flex-row gap-6 mt-6">
// {/* Sidebar actions */}
// <div className="hidden md:flex flex-col gap-4 relative z-10">
// <div className="flex gap-2 items-center">
// <Button
// onClick={handleCopyLink}
// size="lg"
// className="justify-start bg-black text-white rounded-full"
// >
// {copied ? <FaCheck /> : <FaLink />}
// </Button>
// <span>COPY LINK</span>
// </div>
// <div className="flex gap-2 items-center relative">
// <Button
// onClick={() => setShowShareMenu(!showShareMenu)}
// size="lg"
// className="justify-start bg-[#C6A455] text-white rounded-full"
// >
// <FaShareAlt />
// </Button>
// <span>SHARE</span>
// {showShareMenu && (
// <div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48">
// <SocialItem icon={<FaFacebookF />} label="Facebook" />
// <SocialItem icon={<FaTiktok />} label="TikTok" />
// <SocialItem icon={<FaYoutube />} label="YouTube" />
// <SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
// <SocialItem icon={<FaInstagram />} label="Instagram" />
// <SocialItem icon={<FaTwitter />} label="Twitter" />
// </div>
// )}
// </div>
// <div className="flex gap-2 items-center">
// <Link href={`/content/video/comment/${id}`}>
// <Button
// variant="default"
// size="lg"
// className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
// >
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="15"
// height="15"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
// />
// </svg>
// </Button>
// COMMENT
// </Link>
// </div>
// </div>
// {/* Content */}
// <div className="flex-1 space-y-4">
// <h1 className="text-xl font-bold">{data.title}</h1>
// <div className="text-base text-gray-700 leading-relaxed space-y-3">
// <p>{data.description}</p>
// </div>
// {/* Actions bawah */}
// <div className="flex flex-wrap md:flex-row justify-center gap-4 my-20">
// <div className="flex gap-2 items-center">
// <Button
// onClick={handleCopyLink}
// size="lg"
// className="justify-start bg-black text-white rounded-full"
// >
// {copied ? <FaCheck /> : <FaLink />}
// </Button>
// <span>COPY LINK</span>
// </div>
// <div className="flex gap-2 items-center">
// <Button
// onClick={() => setShowShareMenu(!showShareMenu)}
// size="lg"
// className="justify-start bg-[#C6A455] text-white rounded-full"
// >
// <FaShareAlt />
// </Button>
// <span>SHARE</span>
// </div>
// <div className="flex gap-2 items-center">
// <Link href={`/content/video/comment/${id}`}>
// <Button
// variant="default"
// size="lg"
// className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
// >
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="15"
// height="15"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
// />
// </svg>
// </Button>
// COMMENT
// </Link>
// </div>
// </div>
// </div>
// </div>
// </div>
// );
// }

View File

@ -1,9 +1,8 @@
"use client";
import Image from "next/image";
import { Calendar, Clock, Eye } from "lucide-react";
import { useState, useEffect } from "react";
import { Calendar, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useState, useEffect } from "react";
import {
FaFacebookF,
FaTiktok,
@ -15,34 +14,24 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton";
import { getDetail } from "@/service/landing/landing";
import { getArticleDetail } from "@/service/content/content";
export default function DocumentDetail({ id }: { id: string }) {
const [copied, setCopied] = useState(false);
const [showShareMenu, setShowShareMenu] = useState(false);
export default function DocumentDetail({ id }: { id: number }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
const [showShareMenu, setShowShareMenu] = useState(false);
const [selectedDoc, setSelectedDoc] = useState(0);
const [isLoading, setIsLoading] = useState<any>(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
// Copy link
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy: ", err);
console.error("Gagal menyalin:", err);
}
};
@ -63,86 +52,64 @@ export default function DocumentDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
setLoading(true);
// Try new Articles API first
const response = await getArticleDetail(id);
console.log("Article Detail API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
// 1⃣ Coba ambil dari API baru
const res = await getArticleDetail(id);
console.log("Response artikel:", res);
const article = res?.data?.data;
if (article) {
const mappedData = {
id: article.id,
title: article.title,
description: article.description,
createdAt: article.createdAt,
clickCount: article.viewCount,
creatorGroupLevelName: article.createdByName || "Unknown",
uploadedBy: {
publisher: article.createdByName || "MABES POLRI",
},
files:
article.files?.map((f: any) => ({
id: f.id,
fileName: f.file_name,
url: f.file_url,
secondaryUrl: f.file_url,
fileThumbnail: f.file_thumbnail,
fileAlt: f.file_alt,
size: f.size,
createdAt: f.created_at,
updatedAt: f.updated_at,
})) || [],
};
setData(mappedData);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
secondaryUrl: file.file_url, // For document files, use same URL
...file
})) || [],
...articleData
};
setData(transformedData);
console.log(
"doc",
transformedData.files[selectedDoc]?.secondaryUrl
);
}
} catch (error) {
console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
// 2⃣ Fallback ke API lama
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
} catch (err) {
console.error("Gagal ambil detail:", err);
// try {
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
// } catch (err2) {
// console.error("Fallback gagal:", err2);
// }
} finally {
setLoading(false);
}
};
if (id) fetchDetail();
}, [id]);
if (loading) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
<p>Loading...</p>
<p>Memuat data...</p>
</div>
);
}
@ -157,17 +124,32 @@ export default function DocumentDetail({ id }: { id: string }) {
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
<div className="relative">
{/* Viewer dokumen utama */}
{/* <div className="relative">
<iframe
src={data?.files[selectedDoc]?.secondaryUrl}
src={data?.files?.[selectedDoc]?.secondaryUrl || ""}
className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full"
/>
</div> */}
<div className="bg-[#e0c350] flex items-center justify-center h-[170px] text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
width="150"
height="150"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207zM7 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M7.5 9a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM7 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M5.5 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1M6 9.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0M5.5 12a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1"
/>
</svg>
</div>
{/* Pilihan dokumen */}
<div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
{data?.files?.map((file: any, index: number) => (
<button
key={file?.id}
key={file.id}
onClick={() => setSelectedDoc(index)}
className={`px-4 py-2 rounded-md border cursor-pointer hover:bg-gray-100 ${
selectedDoc === index ? "border-red-600 bg-gray-50" : ""
@ -178,110 +160,45 @@ export default function DocumentDetail({ id }: { id: string }) {
))}
</div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between text-sm text-muted-foreground">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
</span>
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
{new Date(data.createdAt)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(".", ":")}{" "}
WIB
</span>
{/* <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Clock className="w-4 h-4" />
{data.time || "-"}
</span> */}
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Eye className="w-4 h-4" />
{data.clickCount || 0}
</span>
<span className="text-black">
{" "}
Creator: {data.creatorGroupLevelName}
</span>
</div>
{/* Informasi artikel */}
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
</span>
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
{new Date(data.createdAt)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(".", ":")}{" "}
WIB
</span>
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Eye className="w-4 h-4" />
{data.clickCount || 0}
</span>
<span className="text-black">
Creator: {data.creatorGroupLevelName}
</span>
</div>
{/* Konten deskripsi */}
<div className="flex flex-col md:flex-row gap-6 mt-6">
{/* Sidebar actions */}
<div className="hidden md:flex flex-col gap-4 relative z-10">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
size="lg"
className="justify-start bg-black text-white rounded-full"
>
{copied ? <FaCheck /> : <FaLink />}
</Button>
<span>COPY LINK</span>
</div>
<div className="flex gap-2 items-center relative">
<Button
onClick={() => setShowShareMenu(!showShareMenu)}
size="lg"
className="justify-start bg-[#C6A455] text-white rounded-full"
>
<FaShareAlt />
</Button>
<span>SHARE</span>
{showShareMenu && (
<div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48">
<SocialItem icon={<FaFacebookF />} label="Facebook" />
<SocialItem icon={<FaTiktok />} label="TikTok" />
<SocialItem icon={<FaYoutube />} label="YouTube" />
<SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
<SocialItem icon={<FaInstagram />} label="Instagram" />
<SocialItem icon={<FaTwitter />} label="Twitter" />
</div>
)}
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
/>
</svg>
</Button>
COMMENT
</Link>
</div>
</div>
{/* Content */}
<div className="flex-1 space-y-4">
<h1 className="text-xl font-bold">{data.title}</h1>
<div className="text-base text-gray-700 leading-relaxed space-y-3">
<p>{data.description}</p>
</div>
{/* Actions bawah */}
<div className="flex flex-wrap md:flex-row justify-center gap-4 my-20">
{/* Tombol aksi bawah */}
<div className="flex flex-wrap justify-center gap-4 my-20">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
@ -301,6 +218,16 @@ export default function DocumentDetail({ id }: { id: string }) {
<FaShareAlt />
</Button>
<span>SHARE</span>
{showShareMenu && (
<div className="absolute mt-10 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48 z-10">
<SocialItem icon={<FaFacebookF />} label="Facebook" />
<SocialItem icon={<FaTiktok />} label="TikTok" />
<SocialItem icon={<FaYoutube />} label="YouTube" />
<SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
<SocialItem icon={<FaInstagram />} label="Instagram" />
<SocialItem icon={<FaTwitter />} label="Twitter" />
</div>
)}
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/video/comment/${id}`}>
@ -309,17 +236,7 @@ export default function DocumentDetail({ id }: { id: string }) {
size="lg"
className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
/>
</svg>
💬
</Button>
COMMENT
</Link>
@ -330,3 +247,333 @@ export default function DocumentDetail({ id }: { id: string }) {
</div>
);
}
// "use client";
// import Image from "next/image";
// import { Calendar, Clock, Eye } from "lucide-react";
// import { Button } from "@/components/ui/button";
// import Link from "next/link";
// import { useState, useEffect } from "react";
// import {
// FaFacebookF,
// FaTiktok,
// FaYoutube,
// FaWhatsapp,
// FaInstagram,
// FaTwitter,
// FaCheck,
// FaLink,
// FaShareAlt,
// } from "react-icons/fa";
// import { getDetail, getArticleDetail } from "@/service/landing/landing";
// export default function DocumentDetail({ id }: { id: string }) {
// const [copied, setCopied] = useState(false);
// const [showShareMenu, setShowShareMenu] = useState(false);
// const [data, setData] = useState<any>(null);
// const [loading, setLoading] = useState(true);
// const [selectedDoc, setSelectedDoc] = useState(0);
// const [isLoading, setIsLoading] = useState<any>(true);
// useEffect(() => {
// const timer = setTimeout(() => {
// setIsLoading(false);
// }, 3000);
// return () => clearTimeout(timer);
// }, []);
// const handleCopyLink = async () => {
// try {
// await navigator.clipboard.writeText(window.location.href);
// setCopied(true);
// setTimeout(() => setCopied(false), 2000);
// } catch (err) {
// console.error("Failed to copy: ", err);
// }
// };
// const SocialItem = ({
// icon,
// label,
// }: {
// icon: React.ReactNode;
// label: string;
// }) => (
// <div className="flex items-center gap-3 cursor-pointer hover:opacity-80">
// <div className="bg-[#C6A455] p-2 rounded-full text-white">{icon}</div>
// <span className="text-sm">{label}</span>
// </div>
// );
// useEffect(() => {
// const fetchDetail = async () => {
// try {
// setLoading(true);
// // Try new Articles API first
// const response = await getArticleDetail(id);
// console.log("Article Detail API response:", response);
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
// console.log(
// "doc",
// fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
// );
// return;
// }
// // Handle new API response structure
// const articleData = response?.data?.data;
// if (articleData) {
// // Transform article data to match old structure for backward compatibility
// const transformedData = {
// id: articleData.id,
// title: articleData.title,
// description: articleData.description,
// createdAt: articleData.createdAt,
// clickCount: articleData.viewCount,
// creatorGroupLevelName: articleData.createdByName || "Unknown",
// uploadedBy: {
// publisher: articleData.createdByName || "MABES POLRI"
// },
// files: articleData.files?.map((file: any) => ({
// id: file.id,
// url: file.file_url,
// fileName: file.file_name,
// filePath: file.file_path,
// fileThumbnail: file.file_thumbnail,
// fileAlt: file.file_alt,
// widthPixel: file.width_pixel,
// heightPixel: file.height_pixel,
// size: file.size,
// downloadCount: file.download_count,
// createdAt: file.created_at,
// updatedAt: file.updated_at,
// secondaryUrl: file.file_url, // For document files, use same URL
// ...file
// })) || [],
// ...articleData
// };
// setData(transformedData);
// console.log(
// "doc",
// transformedData.files[selectedDoc]?.secondaryUrl
// );
// }
// } catch (error) {
// console.error("Error fetching detail:", error);
// // Try fallback to old API if new API fails
// try {
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
// console.log(
// "doc",
// fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
// );
// } catch (fallbackError) {
// console.error("Fallback API also failed:", fallbackError);
// }
// } finally {
// setLoading(false);
// }
// };
// if (id) fetchDetail();
// }, [id]);
// if (loading) {
// return (
// <div className="max-w-6xl mx-auto px-4 py-6">
// <p>Loading...</p>
// </div>
// );
// }
// if (!data) {
// return (
// <div className="max-w-6xl mx-auto px-4 py-6">
// <p>Data tidak ditemukan</p>
// </div>
// );
// }
// return (
// <div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
// <div className="relative">
// <iframe
// src={data?.files[selectedDoc]?.secondaryUrl}
// className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full"
// />
// </div>
// <div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
// {data?.files?.map((file: any, index: number) => (
// <button
// key={file?.id}
// onClick={() => setSelectedDoc(index)}
// className={`px-4 py-2 rounded-md border cursor-pointer hover:bg-gray-100 ${
// selectedDoc === index ? "border-red-600 bg-gray-50" : ""
// }`}
// >
// 📄 {file?.fileName || `Dokumen ${index + 1}`}
// </button>
// ))}
// </div>
// <div className="flex flex-col md:flex-row md:items-center md:justify-between text-sm text-muted-foreground">
// <div className="flex flex-wrap items-center gap-2">
// <span className="font-semibold text-black border-r-2 pr-2 border-black">
// by {data?.uploadedBy?.publisher || "MABES POLRI"}
// </span>
// <span className="flex items-center gap-1 text-black">
// <Calendar className="w-4 h-4" />
// {new Date(data.createdAt)
// .toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// hour12: false,
// timeZone: "Asia/Jakarta",
// })
// .replace(".", ":")}{" "}
// WIB
// </span>
// {/* <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
// <Clock className="w-4 h-4" />
// {data.time || "-"}
// </span> */}
// <span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
// <Eye className="w-4 h-4" />
// {data.clickCount || 0}
// </span>
// <span className="text-black">
// {" "}
// Creator: {data.creatorGroupLevelName}
// </span>
// </div>
// </div>
// <div className="flex flex-col md:flex-row gap-6 mt-6">
// {/* Sidebar actions */}
// <div className="hidden md:flex flex-col gap-4 relative z-10">
// <div className="flex gap-2 items-center">
// <Button
// onClick={handleCopyLink}
// size="lg"
// className="justify-start bg-black text-white rounded-full"
// >
// {copied ? <FaCheck /> : <FaLink />}
// </Button>
// <span>COPY LINK</span>
// </div>
// <div className="flex gap-2 items-center relative">
// <Button
// onClick={() => setShowShareMenu(!showShareMenu)}
// size="lg"
// className="justify-start bg-[#C6A455] text-white rounded-full"
// >
// <FaShareAlt />
// </Button>
// <span>SHARE</span>
// {showShareMenu && (
// <div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48">
// <SocialItem icon={<FaFacebookF />} label="Facebook" />
// <SocialItem icon={<FaTiktok />} label="TikTok" />
// <SocialItem icon={<FaYoutube />} label="YouTube" />
// <SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
// <SocialItem icon={<FaInstagram />} label="Instagram" />
// <SocialItem icon={<FaTwitter />} label="Twitter" />
// </div>
// )}
// </div>
// <div className="flex gap-2 items-center">
// <Link href={`/content/video/comment/${id}`}>
// <Button
// variant="default"
// size="lg"
// className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
// >
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="15"
// height="15"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
// />
// </svg>
// </Button>
// COMMENT
// </Link>
// </div>
// </div>
// {/* Content */}
// <div className="flex-1 space-y-4">
// <h1 className="text-xl font-bold">{data.title}</h1>
// <div className="text-base text-gray-700 leading-relaxed space-y-3">
// <p>{data.description}</p>
// </div>
// {/* Actions bawah */}
// <div className="flex flex-wrap md:flex-row justify-center gap-4 my-20">
// <div className="flex gap-2 items-center">
// <Button
// onClick={handleCopyLink}
// size="lg"
// className="justify-start bg-black text-white rounded-full"
// >
// {copied ? <FaCheck /> : <FaLink />}
// </Button>
// <span>COPY LINK</span>
// </div>
// <div className="flex gap-2 items-center">
// <Button
// onClick={() => setShowShareMenu(!showShareMenu)}
// size="lg"
// className="justify-start bg-[#C6A455] text-white rounded-full"
// >
// <FaShareAlt />
// </Button>
// <span>SHARE</span>
// </div>
// <div className="flex gap-2 items-center">
// <Link href={`/content/video/comment/${id}`}>
// <Button
// variant="default"
// size="lg"
// className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
// >
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="15"
// height="15"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8"
// />
// </svg>
// </Button>
// COMMENT
// </Link>
// </div>
// </div>
// </div>
// </div>
// </div>
// );
// }

View File

@ -1,45 +1,90 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import CategoryTabs from "./category-tabs";
import PublicationCardGrid from "./publication-card";
import Cookies from "js-cookie";
import SidebarFilterForYou from "./sidebar-filter-for-you";
import { useRouter } from "next/navigation";
import ForYouCardGrid from "./for-you-card";
import { getCookiesDecrypt } from "@/lib/utils";
export default function PublicationKlLayout() {
const [selectedCategory, setSelectedCategory] = useState("SEMUA");
const router = useRouter();
// useEffect(() => {
// const roleId = getCookiesDecrypt("urie") ?? "";
// const allowedRoles = ["6", "7", "2", "3"];
// if (!allowedRoles.includes(roleId)) {
// router.push("/auth");
// }
// }, [router]);
// 🔹 Tab filter untuk SidebarFilterForYou
const [activeTab, setActiveTab] = useState<
"My Collections" | "Archives" | "Favorites"
>("My Collections");
return (
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
{/* Kategori utama (SEMUA, POLDA, SATKER, dst) */}
<CategoryTabs
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
/>
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar Filter */}
<div className="lg:w-1/4 xl:w-1/5 w-full">
<div className="border rounded p-4 bg-white">
<SidebarFilterForYou />
<SidebarFilterForYou
activeTab={activeTab}
onTabChange={(tab) =>
setActiveTab(tab as "My Collections" | "Archives" | "Favorites")
}
/>
</div>
</div>
{/* Grid Konten */}
<div className="flex-1">
<ForYouCardGrid selectedCategory={selectedCategory} />
<ForYouCardGrid
selectedCategory={selectedCategory}
filterType={activeTab}
/>
</div>
</div>
</div>
);
}
// "use client";
// import { useState } from "react";
// import CategoryTabs from "./category-tabs";
// import SidebarFilterForYou from "./sidebar-filter-for-you";
// import { useRouter } from "next/navigation";
// import ForYouCardGrid from "./for-you-card";
// export default function PublicationKlLayout() {
// const [selectedCategory, setSelectedCategory] = useState("SEMUA");
// const router = useRouter();
// // useEffect(() => {
// // const roleId = getCookiesDecrypt("urie") ?? "";
// // const allowedRoles = ["6", "7", "2", "3"];
// // if (!allowedRoles.includes(roleId)) {
// // router.push("/auth");
// // }
// // }, [router]);
// return (
// <div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
// <CategoryTabs
// selectedCategory={selectedCategory}
// onCategoryChange={setSelectedCategory}
// />
// <div className="flex flex-col lg:flex-row gap-6">
// <div className="lg:w-1/4 xl:w-1/5 w-full">
// <div className="border rounded p-4 bg-white">
// <SidebarFilterForYou />
// </div>
// </div>
// <div className="flex-1">
// <ForYouCardGrid selectedCategory={selectedCategory} />
// </div>
// </div>
// </div>
// );
// }

View File

@ -1,14 +1,18 @@
"use client";
import { useEffect, useState } from "react";
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ThumbsUp, ThumbsDown, Trash2 } from "lucide-react";
import Link from "next/link";
import { getBookmarks, toggleBookmarkById, BookmarkItem } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Archive, Star } from "lucide-react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getBookmarks } from "@/service/content";
import { BookmarkItem } from "@/service/content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import { Navigation } from "swiper/modules";
// Format tanggal
function formatTanggal(dateString: string) {
@ -28,7 +32,6 @@ function formatTanggal(dateString: string) {
);
}
// Function to get link based on typeId
function getLink(item: BookmarkItem) {
switch (item.article?.typeId) {
case 1:
@ -44,236 +47,234 @@ function getLink(item: BookmarkItem) {
}
}
// Function to get content type label
function getContentTypeLabel(typeId: number) {
switch (typeId) {
case 1:
return "📸 Image";
case 2:
return "🎬 Video";
case 3:
return "📝 Text";
case 4:
return "🎵 Audio";
default:
return "📄 Content";
}
}
type PublicationCardGridProps = {
selectedCategory: string;
title?: string;
refresh?: boolean;
categoryFilter?: string[];
formatFilter?: string[];
isInstitute?: boolean;
instituteId?: string;
sortBy?: string;
type ForYouCardGridProps = {
filterType: "My Collections" | "Archives" | "Favorites";
selectedCategory?: string;
};
export default function ForYouCardGrid({
selectedCategory,
title,
refresh,
isInstitute = false,
instituteId = "",
}: PublicationCardGridProps) {
export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [limit] = useState(12);
const MySwal = withReactContent(Swal);
useEffect(() => {
fetchBookmarks();
}, [currentPage, selectedCategory, title, refresh]);
}, [filterType]);
const fetchBookmarks = async () => {
try {
setLoading(true);
const response = await getBookmarks(currentPage, limit);
const response = await getBookmarks(1, 50, filterType);
if (!response?.error) {
setBookmarks(response.data?.data || []);
setTotalPages(response.data?.meta?.totalPage || 1);
setTotalCount(response.data?.meta?.count || 0);
} else {
console.error("Failed to fetch bookmarks:", response?.error);
MySwal.fire({
icon: "error",
title: "Error",
text: "Gagal memuat bookmark.",
confirmButtonColor: "#d33",
});
MySwal.fire("Error", "Gagal memuat bookmark.", "error");
}
} catch (err) {
console.error("Error fetching bookmarks:", err);
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan saat memuat bookmark.",
confirmButtonColor: "#d33",
});
console.error(err);
MySwal.fire("Error", "Terjadi kesalahan saat memuat bookmark.", "error");
} finally {
setLoading(false);
}
};
const handleDeleteBookmark = async (bookmarkId: number, articleId: number, articleTitle: string) => {
const result = await MySwal.fire({
title: "Hapus Bookmark",
text: `Apakah Anda yakin ingin menghapus "${articleTitle}" dari bookmark?`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Ya, Hapus!",
cancelButtonText: "Batal",
});
if (result.isConfirmed) {
try {
const response = await toggleBookmarkById(articleId);
if (response?.data?.success || !response?.error) {
// Remove from local state
setBookmarks(prev => prev.filter(bookmark => bookmark.id !== bookmarkId));
setTotalCount(prev => prev - 1);
MySwal.fire({
icon: "success",
title: "Berhasil",
text: "Bookmark berhasil dihapus.",
timer: 1500,
showConfirmButton: false,
});
} else {
MySwal.fire({
icon: "error",
title: "Gagal",
text: "Gagal menghapus bookmark.",
confirmButtonColor: "#d33",
});
}
} catch (err) {
console.error("Error deleting bookmark:", err);
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan saat menghapus bookmark.",
confirmButtonColor: "#d33",
});
}
}
const grouped = {
image: bookmarks.filter((b) => b.article?.typeId === 1).slice(0, ),
video: bookmarks.filter((b) => b.article?.typeId === 2).slice(0, 5),
text: bookmarks.filter((b) => b.article?.typeId === 3).slice(0, 5),
audio: bookmarks.filter((b) => b.article?.typeId === 4).slice(0, 5),
};
const goToPage = (page: number) => {
setCurrentPage(page);
};
const renderImageVideoCard = (bookmark: BookmarkItem) => (
<div className="rounded-xl shadow-md bg-white hover:shadow-lg transition-shadow overflow-hidden">
<div className="relative w-full h-[200px]">
<Link href={getLink(bookmark)}>
<Image
src={bookmark.article?.thumbnailUrl || "/placeholder.png"}
alt={bookmark.article?.title || "No Title"}
fill
className="object-cover"
/>
</Link>
</div>
<div className="p-3">
<p className="text-xs text-gray-500 mb-1">
Disimpan: {formatTanggal(bookmark.createdAt)}
</p>
<Link href={getLink(bookmark)}>
<p className="text-sm font-semibold line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{bookmark.article?.title}
</p>
</Link>
<div className="flex gap-2 mt-3">
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-gray-700 border-gray-300 hover:bg-gray-100"
>
<Archive className="w-4 h-4" /> Archive
</Button>
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50"
>
<Star className="w-4 h-4" /> Favorite
</Button>
</div>
</div>
</div>
);
const renderTextCard = (bookmark: BookmarkItem) => (
<div className="cursor-pointer rounded-lg shadow-md overflow-hidden bg-white dark:bg-black dark:border dark:border-gray-500 hover:shadow-lg transition-shadow">
{/* Background kuning & ikon dokumen */}
<div className="bg-[#e0c350] flex items-center justify-center h-[170px] text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z"
/>
</svg>
</div>
{/* Konten bawah */}
<div className="p-4 flex flex-col gap-2">
<p className="text-xs text-gray-500 mb-1">
Disimpan: {formatTanggal(bookmark.createdAt)}
</p>
<Link
href={getLink(bookmark)}
className="font-semibold text-gray-900 dark:text-white text-base leading-snug line-clamp-3"
>
{bookmark.article?.title}
</Link>
<div className="flex gap-2 mt-3">
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-gray-700 border-gray-300 hover:bg-gray-100"
>
<Archive className="w-4 h-4" /> Archive
</Button>
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50"
>
<Star className="w-4 h-4" /> Favorite
</Button>
</div>
</div>
</div>
);
const renderAudioCard = (bookmark: BookmarkItem) => (
<div className="cursor-pointer bg-white dark:bg-black dark:border dark:border-gray-500 rounded-xl shadow-md hover:shadow-lg transition-shadow overflow-hidden">
{/* Background merah & ikon audio */}
<div className="flex items-center justify-center bg-[#bb3523] w-full h-[170px] text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
width="120"
height="120"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/>
</svg>
</div>
{/* Caption */}
<Link href={getLink(bookmark)} className="p-4">
<p className="text-xs text-gray-500 mb-1">
Disimpan: {formatTanggal(bookmark.createdAt)}
</p>
<p className="text-base font-semibold text-black dark:text-white line-clamp-3">
{bookmark.article?.title}
</p>
</Link>
<div className="flex gap-2 mx-2">
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-gray-700 border-gray-300 hover:bg-gray-100"
>
<Archive className="w-4 h-4" /> Archive
</Button>
<Button
size="sm"
variant="outline"
className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50"
>
<Star className="w-4 h-4" /> Favorite
</Button>
</div>
</div>
);
const renderSection = (
title: string,
items: BookmarkItem[],
type: "imagevideo" | "text" | "audio"
) => {
if (!items.length) return null;
if (loading) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="rounded-xl shadow-md overflow-hidden bg-white animate-pulse">
<div className="w-full h-[204px] bg-gray-200"></div>
<div className="p-3 space-y-2">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
<div className="mb-10">
<h2 className="text-xl font-bold text-gray-800 mb-4">{title}</h2>
<Swiper
spaceBetween={20}
slidesPerView={1.2}
breakpoints={{
640: { slidesPerView: 2.2 },
1024: { slidesPerView: 3.2 },
1280: { slidesPerView: 4.2 },
}}
navigation
modules={[Navigation]}
>
{items.map((bookmark) => (
<SwiperSlide key={bookmark.id}>
{type === "imagevideo"
? renderImageVideoCard(bookmark)
: type === "text"
? renderTextCard(bookmark)
: renderAudioCard(bookmark)}
</SwiperSlide>
))}
</Swiper>
</div>
);
}
};
if (bookmarks.length === 0) {
if (loading)
return (
<div className="text-center py-12 text-gray-500">Memuat konten...</div>
);
if (bookmarks.length === 0)
return (
<div className="text-center py-12">
<div className="text-gray-400 text-6xl mb-4">📚</div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">Tidak Ada Bookmark</h3>
<p className="text-gray-500">Anda belum menyimpan artikel apapun ke bookmark.</p>
<div className="text-gray-400 text-6xl mb-4">📂</div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">
Tidak Ada Konten di {filterType}
</h3>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-semibold text-gray-800 mb-2">Bookmark Saya</h2>
<p className="text-gray-600">Total {totalCount} artikel tersimpan</p>
</div>
{/* Grid Card */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{bookmarks.map((bookmark) => (
<div key={bookmark.id} className="rounded-xl shadow-md overflow-hidden bg-white hover:shadow-lg transition-shadow">
<div className="w-full h-[204px] relative">
<Link href={getLink(bookmark)}>
<Image
src={bookmark.article?.thumbnailUrl || "/placeholder.png"}
alt={bookmark.article?.title || "No Title"}
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/>
</Link>
</div>
<div className="p-3">
<div className="flex gap-2 mb-1">
<span className="text-xs text-white px-2 py-0.5 rounded bg-blue-600">
{getContentTypeLabel(bookmark.article?.typeId || 0)}
</span>
{/* <span className="text-xs font-medium text-[#b3882e]">
Bookmark
</span> */}
</div>
<p className="text-xs text-gray-500 mb-1">
Disimpan: {formatTanggal(bookmark.createdAt)}
</p>
<Link href={getLink(bookmark)}>
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{bookmark.article?.title}
</p>
</Link>
<div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer hover:text-green-600" />
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
</div>
<Button
onClick={() => handleDeleteBookmark(bookmark.id, bookmark.article?.id || 0, bookmark.article?.title || "")}
variant="outline"
size="sm"
className="rounded px-3 text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300"
>
<Trash2 className="w-4 h-4 mr-1" />
Hapus
</Button>
</div>
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex justify-center gap-2">
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="sm"
onClick={() => goToPage(i + 1)}
>
{i + 1}
</Button>
))}
</div>
<div className="space-y-10">
{renderSection("📸 Image", grouped.image, "imagevideo")}
{renderSection("🎬 Video", grouped.video, "imagevideo")}
{renderSection("📝 Text", grouped.text, "text")}
{renderSection("🎵 Audio", grouped.audio, "audio")}
</div>
);
}

View File

@ -1,10 +1,18 @@
import { Yarndings_12 } from "next/font/google";
import { BurgerButtonIcon } from "../icons";
"use client";
import { Plus } from "lucide-react";
const filters = ["My Collections", "Archives", "Favorites", "Highlight"];
const filters = ["My Collections", "Archives", "Favorites"];
export default function SidebarFilterForYou() {
type SidebarFilterForYouProps = {
activeTab: string;
onTabChange: (tab: string) => void;
};
export default function SidebarFilterForYou({
activeTab,
onTabChange,
}: SidebarFilterForYouProps) {
return (
<div>
<div className="flex gap-2 items-center border-b-2 mb-3">
@ -13,7 +21,13 @@ export default function SidebarFilterForYou() {
</div>
<ul className="space-y-6 text-[16px] text-[#757575]">
{filters.map((item, idx) => (
<li key={idx} className="cursor-pointer hover:font-semibold">
<li
key={idx}
onClick={() => onTabChange(item)}
className={`cursor-pointer hover:font-semibold ${
activeTab === item ? "font-semibold text-black" : ""
}`}
>
{item}
</li>
))}
@ -21,3 +35,27 @@ export default function SidebarFilterForYou() {
</div>
);
}
// import { Yarndings_12 } from "next/font/google";
// import { BurgerButtonIcon } from "../icons";
// import { Plus } from "lucide-react";
// const filters = ["My Collections", "Archives", "Favorites", "Highlight"];
// export default function SidebarFilterForYou() {
// return (
// <div>
// <div className="flex gap-2 items-center border-b-2 mb-3">
// <h2 className="font-semibold text-lg">Create New Collection </h2>
// <Plus size={20} />
// </div>
// <ul className="space-y-6 text-[16px] text-[#757575]">
// {filters.map((item, idx) => (
// <li key={idx} className="cursor-pointer hover:font-semibold">
// {item}
// </li>
// ))}
// </ul>
// </div>
// );
// }

View File

@ -474,7 +474,26 @@ export interface BookmarksResponse {
};
}
export async function getBookmarks(page = 1, limit = 10) {
const url = `bookmarks?page=${page}&limit=${limit}`;
// export async function getBookmarks(page = 1, limit = 10) {
// const url = `bookmarks?page=${page}&limit=${limit}`;
// return httpGetInterceptor(url);
// }
export async function getBookmarks(
page = 1,
limit = 10,
filterType?: "My Collections" | "Archives" | "Favorites"
) {
let url = `bookmarks?page=${page}&limit=${limit}`;
// Tambahkan filter berdasarkan tab aktif
if (filterType === "Archives") {
url += `&status=archived`;
} else if (filterType === "Favorites") {
url += `&isFavorite=true`;
} else {
url += `&status=active`;
}
return httpGetInterceptor(url);
}
}