fix: detail content image

This commit is contained in:
Sabda Yagra 2025-10-12 22:12:07 +07:00
parent e8f49bdad4
commit 6e39ec873e
13 changed files with 1694 additions and 282 deletions

View File

@ -0,0 +1,912 @@
"use client";
import React, { useEffect, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@iconify/react/dist/iconify.js";
import {
formatDateToIndonesian,
getOnlyDate,
getOnlyMonthAndYear,
} from "@/utils/globals";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import {
getPublicCategoryData,
getUserLevelListByParent,
listCategory,
listData,
listDataRegional,
} from "@/service/landing/landing";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Link, useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import ReactDatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { close, loading } from "@/config/swal";
import { useTranslations } from "next-intl";
import ImageBlurry from "@/components/ui/image-blurry";
import { Skeleton } from "@/components/ui/skeleton";
import Image from "next/image";
import LandingPagination from "@/components/landing-page/pagination";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
];
const FilterPage = () => {
const router = useRouter();
const asPath = usePathname();
const params = useParams();
const searchParams = useSearchParams();
const locale = params?.locale;
const [isLoading, setIsLoading] = useState<any>(true);
const [imageData, setImageData] = useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = useState(1);
const [totalContent, setTotalContent] = useState();
const [change, setChange] = useState(false);
const sortBy = searchParams?.get("sortBy");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const group = searchParams?.get("group");
const [contentImage, setContentImage] = useState([]);
const [, setGetTotalPage] = useState();
let typingTimer: any;
const doneTypingInterval = 1500;
const [search, setSearch] = useState();
const [categoryFilter, setCategoryFilter] = useState<any>([]);
const [monthYearFilter, setMonthYearFilter] = useState<any>();
const [searchTitle, setSearchTitle] = useState<string>("");
const [sortByOpt, setSortByOpt] = useState<any>(
sortBy === "popular" ? "clickCount" : "createdAt"
);
const isRegional = asPath?.includes("regional");
const isSatker = asPath?.includes("satker");
const [formatFilter, setFormatFilter] = useState<any>([]);
const pages = page ? page - 1 : 0;
const [startDateString, setStartDateString] = useState<any>();
const [endDateString, setEndDateString] = useState<any>();
const [dateRange, setDateRange] = useState<any>([null, null]);
const [calenderState, setCalenderState] = useState(false);
const [handleClose, setHandleClose] = useState(false);
const [categories, setCategories] = useState([]);
const [userLevels, setUserLevels] = useState([]);
const t = useTranslations("FilterPage");
const [isFilterOpen, setIsFilterOpen] = useState(true);
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
const [categoryPage, setCategoryPage] = useState(1);
const [categoryTotalPages, setCategoryTotalPages] = useState(1);
// const [startDate, endDate] = dateRange;
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
useEffect(() => {
async function initState() {
// getCategories();
// getSelectedCategory();
if (isSatker) {
getUserLevels();
}
}
initState();
}, []);
useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
// useEffect(() => {
// fetchData();
// }, [page, sortBy, sortByOpt, title]);
useEffect(() => {
async function initState() {
if (isRegional) {
getDataRegional();
} else {
getDataAll();
}
}
console.log(monthYearFilter, "monthFilter");
initState();
}, [
change,
asPath,
monthYearFilter,
page,
sortBy,
sortByOpt,
title,
startDateString,
endDateString,
categorie,
formatFilter,
]);
// async function getCategories() {
// const category = await listCategory("1");
// const resCategory = category?.data?.data?.content;
// setCategories(resCategory);
// }
// useEffect(() => {
// initFetch();
// }, []);
// const initFetch = async () => {
// const response = await getPublicCategoryData(
// poldaName && String(poldaName)?.length > 1
// ? poldaName
// : satkerName && String(satkerName)?.length > 1
// ? "satker-" + satkerName
// : "",
// "",
// locale == "en" ? true : false
// );
// console.log("category", response);
// setCategories(response?.data?.data?.content);
// };
useEffect(() => {
fetchCategories(categoryPage);
}, [categoryPage]);
const fetchCategories = async (pageNumber: number) => {
const groupParam =
poldaName && poldaName.length > 1
? poldaName
: satkerName && satkerName.length > 1
? "satker-" + satkerName
: "";
const isInt = locale === "en";
const response = await getPublicCategoryData(
groupParam,
"",
isInt,
pageNumber
);
const content = response?.data?.data?.content || [];
const total = response?.data?.data?.totalPages || 1;
setCategories(content);
setCategoryTotalPages(total);
};
useEffect(() => {
function initState() {
if (dateRange[0] != null && dateRange[1] != null) {
setStartDateString(getOnlyDate(dateRange[0]));
setEndDateString(getOnlyDate(dateRange[1]));
setHandleClose(true);
console.log("date range", dateRange, getOnlyDate(dateRange[0]));
}
}
initState();
}, [calenderState]);
async function getDataAll() {
if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
const filterGroup = group == undefined ? asPath.split("/")[2] : group;
loading();
const response = await listData(
"1",
name,
filter,
12,
pages,
sortByOpt,
format,
"",
filterGroup,
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)
?.split("/")[0]
?.replace("", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: "",
locale == "en" ? true : false
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
} else {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listData(
"1",
name,
filter,
12,
pages,
sortByOpt,
format,
"",
"",
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[0]?.replace("", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: "",
locale == "en" ? true : false
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
}
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...filter, String(id)];
} else {
filter = filter.filter((item) => item !== String(id));
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const handleFormatFilter = (e: boolean, id: string) => {
let filter = [...formatFilter];
if (e) {
filter = [...formatFilter, id];
} else {
filter.splice(formatFilter.indexOf(id), 1);
}
console.log("Format filter", filter);
setFormatFilter(filter);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
setDateRange([null, null]);
setMonthYearFilter(null);
setChange(!change);
};
async function getDataRegional() {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listDataRegional(
"1",
name,
filter,
format,
"",
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[0]?.replace("", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: "",
12,
pages,
sortByOpt
);
close();
setGetTotalPage(response?.data?.data?.totalPages);
setContentImage(response?.data?.data?.content);
setTotalContent(response?.data?.data?.totalElements);
}
const table = useReactTable({
data: imageData,
columns: columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
function getSelectedCategory() {
const filter = [];
if (categorie) {
const categoryArr = categorie.split(",");
for (const element of categoryArr) {
filter.push(Number(element));
}
setCategoryFilter(filter);
}
}
const handleDeleteDate = () => {
setDateRange([null, null]);
setStartDateString("");
setEndDateString("");
setHandleClose(false);
};
const handleSorting = (e: any) => {
console.log(e.target.value);
if (e.target.value == "terbaru") {
setSortByOpt("createdAt");
} else {
setSortByOpt("clickCount");
}
setChange(!change);
};
async function getUserLevels() {
const res = await getUserLevelListByParent(761);
const userLevelList = res?.data?.data;
if (userLevelList !== null) {
let optionArr: any = [];
userLevelList?.map((option: any) => {
let optionData = {
id: option.id,
label: option.name,
value: option.id,
};
optionArr.push(optionData);
});
setUserLevels(optionArr);
}
}
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle.toLowerCase()}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
// const shimmer = (w: number, h: number) => `
// <svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
// <defs>
// <linearGradient id="g">
// <stop stop-color="#bcbcbd" offset="20%" />
// <stop stop-color="#f9fafb" offset="50%" />
// <stop stop-color="#bcbcbd" offset="70%" />
// </linearGradient>
// </defs>
// <rect width="${w}" height="${h}" fill="#bcbcbd" />
// <rect id="r" width="${w}" height="${h}" fill="url(#g)" />
// <animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
// </svg>`;
// const toBase64 = (str: string) =>
// typeof window === "undefined"
// ? Buffer.from(str).toString("base64")
// : window.btoa(str);
return (
<div className="flex flex-col">
{/* Header */}
<div className="flex flex-row md:flex-row items-start gap-3 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> {t("image", { defaultValue: "Image" })}</p>
{">"}
<p>
<span className="font-bold">
{t("allImage", { defaultValue: "All Image" })}
</span>
</p>
<p className="font-bold">|</p>
{!title ? (
<p>
{`${t("thereIs", { defaultValue: "Terdapat" })} ${totalContent} ${t(
"downloadableImage",
{ defaultValue: "artikel berisi Foto yang dapat diunduh" }
)}`}
</p>
) : (
<>
<p>
{t("search-results", { defaultValue: "Hasil pencarian untuk" })}{" "}
<span className="font-bold">"{title}"</span>
</p>
</>
)}
</div>
<div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4">
{/* Left */}
<div className="lg:hidden flex justify-end mb-2">
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className="text-sm text-white bg-[#bb3523] px-4 py-1 rounded-md shadow"
>
{isFilterOpen ? "Hide Filter" : "Show Filter"}
</button>
</div>
{isFilterOpen && (
<div className="h-fit min-w-full lg:min-w-[280px] max-w-full lg:max-w-[300px] bg-[#f7f7f7] dark:bg-black p-4 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-1">
<Icon icon="stash:filter-light" fontSize={30} />
Filter
</h2>
<div className="border-t border-black my-4 dark:border-white"></div>
<div className="space-y-6">
<div>
<label
htmlFor="search"
className="block text-sm font-medium text-gray-700 dark:text-white"
>
{t("search", { defaultValue: "Search" })}
</label>
<Input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
id="search"
placeholder={t("searchTitle", {
defaultValue: "Search Title",
})}
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("monthYear", { defaultValue: "Month Year" })}
</label>
<ReactDatePicker
selected={monthYearFilter}
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy"
placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("date", { defaultValue: "Date" })}
</label>
<div className="flex flex-row justify justify-between gap-2">
<ReactDatePicker
selectsRange
className="mt-1 w-full border text-sm rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
startDate={dateRange[0]}
endDate={dateRange[1]}
onChange={(update) => {
setDateRange(update);
}}
placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)}
/>
<div className="flex items-center">
{handleClose ? (
<Icon
icon="carbon:close-filled"
onClick={handleDeleteDate}
width="20"
inline
color="#216ba5"
/>
) : (
""
)}
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
{t("categories", { defaultValue: "Categories" })}
</h3>
<ul className="mt-2 space-y-2">
{categories.map((category: any) => (
<li key={category?.id}>
<label
className="inline-flex items-center"
htmlFor={`${category.id}`}
>
<Checkbox
id={`${category.id}`}
value={category.id}
checked={categoryFilter.includes(String(category.id))}
onCheckedChange={(e) =>
handleCategoryFilter(Boolean(e), category.id)
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
{category?.name}
</span>
</label>
</li>
))}
<div className="mt-4 flex gap-2 justify-center items-center">
<button
onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1))
}
disabled={categoryPage === 1}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.15 16.15l-3.625-3.625q-.125-.125-.175-.25T9.3 12t.05-.275t.175-.25L13.15 7.85q.075-.075.163-.112T13.5 7.7q.2 0 .35.138T14 8.2v7.6q0 .225-.15.363t-.35.137q-.05 0-.35-.15"
/>
</svg>
</button>
{Array.from({ length: categoryTotalPages }, (_, i) => (
<button
key={i}
onClick={() => setCategoryPage(i + 1)}
className={`px-3 py-1 border rounded ${
categoryPage === i + 1
? "bg-[#bb3523] text-white"
: ""
}`}
>
{i + 1}
</button>
))}
<button
onClick={() =>
setCategoryPage((prev) =>
Math.min(prev + 1, categoryTotalPages)
)
}
disabled={categoryPage === categoryTotalPages}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.5 16.3q-.2 0-.35-.137T10 15.8V8.2q0-.225.15-.362t.35-.138q.05 0 .35.15l3.625 3.625q.125.125.175.25t.05.275t-.05.275t-.175.25L10.85 16.15q-.075.075-.162.113t-.188.037"
/>
</svg>
</button>
</div>
</ul>
</div>
{/* Garis */}
<div className="border-t border-black my-4 dark:border-white"></div>
{/* Garis */}
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
Format
</h3>
<ul className="mt-2 space-y-2">
<li>
<label className="inline-flex items-center">
<Checkbox
id="png"
value="png"
checked={formatFilter.includes("png")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "png")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
PNG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpeg"
value="jpeg"
checked={formatFilter.includes("jpeg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpeg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPEG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpg"
value="jpg"
checked={formatFilter.includes("jpg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPG
</span>
</label>
</li>
</ul>
</div>
<div className="border-t border-black dark:border-white my-4"></div>
<div className="text-center">
<a
onClick={cleanCheckbox}
className="text-[#bb3523] cursor-pointer"
>
<b>Reset Filter</b>
</a>
</div>
</div>
</div>
)}
{/* Right */}
<div className="w-full pr-4 lg:pr-16 pb-4">
<div className="w-full">
<div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
>
<option value="latest">
{t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select>
</div>
{isLoading ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col lg:flex-row space-y-3 w-full justify-center items-center gap-3">
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
</div>
<div className="flex flex-col lg:flex-row space-y-3 w-full justify-center items-center gap-3">
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
<Skeleton className="h-[200px] w-[350px] rounded-xl" />
</div>
</div>
) : (
<>
{imageData?.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{imageData?.map((image: any) => (
<Card
key={image?.id}
className="hover:scale-105 transition-transform duration-300"
>
<CardContent className="flex flex-col text-xs lg:text-sm w-full p-0">
<Link href={`/image/detail/${image?.slug}`}>
{/* <img src={image?.thumbnailLink} className="h-60 object-cover items-center justify-center cursor-pointer rounded-lg" /> */}
<div className="img-container h-60 bg-[#e9e9e9] cursor-pointer">
<ImageBlurry
src={
image?.smallThumbnailLink ||
image?.thumbnailLink
}
alt={image?.title}
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</div>
{/* <div className="flex flex-row items-center gap-2 text-[10px] mx-1 mt-2">
{formatDateToIndonesian(
new Date(image?.createdAt)
)}{" "}
{image?.timezone ? image?.timezone : "WIB"}
&nbsp; |
<Icon
icon="formkit:eye"
width="15"
height="15"
/>
{image?.clickCount}{" "}
</div>
<div className="font-semibold pr-3 pb-3 mx-2 hover:h-auto truncate hover:whitespace-normal hover:overflow-visible w-full">
{image?.title}
</div> */}
<div className="p-4 h-full flex flex-col justify-between">
<div className="flex flex-col gap-1 flex-grow">
<div className="flex flex-row justify-between">
<p className="text-[9px] font-bold text-[#bb3523]">
{image?.categoryName?.toUpperCase() ??
"Giat Pimpinan"}
</p>
<p className="flex flex-row items-center text-[9px] gap-1 text-gray-600">
{formatDateToIndonesian(
new Date(image?.createdAt)
)}{" "}
{image?.timezone ?? "WIB"} |
<Icon
icon="formkit:eye"
width="15"
height="15"
/>{" "}
{image.clickCount}
</p>
</div>
<p className="text-sm lg:text-base font-semibold text-black dark:text-white line-clamp-4">
{image?.title}
</p>
</div>
</div>
</Link>
</CardContent>
</Card>
))}
</div>
) : (
<p className="flex items-center justify-center text-black">
<Image
width={1920}
height={1080}
src="/assets/empty-data.png"
alt="empty"
className="h-60 w-60 my-4"
/>
</p>
)}
</>
)}
{totalData > 1 && (
<LandingPagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
)}
</div>
</div>
</div>
</div>
);
};
export default FilterPage;

View File

@ -3,20 +3,21 @@
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { Card } from "../ui/card";
import Link from "next/link";
import { listData, listArticles } from "@/service/landing/landing";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import { Navigation } from "swiper/modules";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { listData, listArticles } from "@/service/landing/landing";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Link from "next/link";
import { ThumbsUp, ThumbsDown } from "lucide-react";
// Format tanggal
// 🔹 Fungsi format tanggal ke WIB
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
@ -35,82 +36,88 @@ function formatTanggal(dateString: string) {
}
export default function MediaUpdate() {
const [tab, setTab] = useState<"latest" | "popular">("latest");
const [dataToRender, setDataToRender] = useState<any[]>([]);
const [tipeKonten, setTipeKonten] = useState<
"image" | "video" | "text" | "audio"
>("image");
const [urutan, setUrutan] = useState<"latest" | "popular">("latest");
const [dataKonten, setDataKonten] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const MySwal = withReactContent(Swal);
useEffect(() => {
fetchData(tab);
}, [tab]);
// 🔹 Pemetaan tipe konten ke typeId API
const typeMap: Record<typeof tipeKonten, number> = {
image: 1,
video: 2,
text: 3,
audio: 4,
};
async function fetchData(section: "latest" | "popular") {
useEffect(() => {
ambilData();
}, [tipeKonten, urutan]);
async function ambilData() {
try {
setLoading(true);
// 🔹 Ambil data artikel
const typeId = typeMap[tipeKonten];
const sortBy = urutan === "latest" ? "createdAt" : "viewCount";
// 🔹 Panggil API baru
const response = await listArticles(
1,
20,
1, // typeId = image
typeId,
undefined,
undefined,
section === "latest" ? "createdAt" : "viewCount"
sortBy
);
let articlesData: any[] = [];
let hasil: any[] = [];
if (response?.error) {
console.error("Articles API failed, fallback ke old API");
const fallbackRes = await listData(
"1",
console.error(
"Gagal ambil data dari listArticles, fallback ke listData"
);
const fallback = await listData(
String(typeId),
"",
"",
20,
0,
section === "latest" ? "createdAt" : "clickCount",
urutan === "latest" ? "createdAt" : "clickCount",
"",
"",
""
);
articlesData = fallbackRes?.data?.data?.content || [];
hasil = fallback?.data?.data?.content || [];
} else {
articlesData = response?.data?.data || [];
hasil = response?.data?.data || [];
}
// 🔹 Normalisasi struktur data
const transformedData = articlesData.map((article: any) => ({
id: article.id,
title: article.title,
// 🔹 Normalisasi data artikel
const dataBaru = hasil.map((a: any) => ({
id: a.id,
title: a.title,
category:
article.categoryName ||
(article.categories && article.categories[0]?.title) ||
a.categoryName ||
(a.categories && a.categories[0]?.title) ||
"Tanpa Kategori",
createdAt: article.createdAt,
smallThumbnailLink: article.thumbnailUrl,
label:
article.typeId === 1
? "Image"
: article.typeId === 2
? "Video"
: article.typeId === 3
? "Text"
: article.typeId === 4
? "Audio"
: "",
...article,
createdAt: a.createdAt,
smallThumbnailLink: a.thumbnailUrl,
typeId: a.typeId,
}));
setDataToRender(transformedData);
setDataKonten(dataBaru);
// 🔹 Sinkronisasi bookmark
// 🔹 Sinkronisasi data bookmark
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
const savedLocal = localStorage.getItem("bookmarkedIds");
const simpananLocal = localStorage.getItem("bookmarkedIds");
let localSet = new Set<number>();
if (savedLocal) {
localSet = new Set(JSON.parse(savedLocal));
if (simpananLocal) {
localSet = new Set(JSON.parse(simpananLocal));
setBookmarkedIds(localSet);
}
@ -120,18 +127,17 @@ export default function MediaUpdate() {
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 merged = new Set([...localSet, ...ids]);
setBookmarkedIds(merged);
const gabungan = new Set([...localSet, ...ids]);
setBookmarkedIds(gabungan);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(merged))
JSON.stringify(Array.from(gabungan))
);
}
} catch (err) {
@ -141,16 +147,7 @@ export default function MediaUpdate() {
}
}
// 🔹 Simpan perubahan bookmark ke localStorage
useEffect(() => {
if (bookmarkedIds.size > 0) {
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(bookmarkedIds))
);
}
}, [bookmarkedIds]);
// 🔹 Simpan bookmark
const handleSave = async (id: number) => {
const roleId = Number(getCookiesDecrypt("urie"));
if (!roleId || isNaN(roleId)) {
@ -170,7 +167,7 @@ export default function MediaUpdate() {
MySwal.fire({
icon: "error",
title: "Gagal",
text: "Gagal menyimpan artikel.",
text: "Tidak dapat menyimpan artikel.",
confirmButtonColor: "#d33",
});
} else {
@ -191,7 +188,7 @@ export default function MediaUpdate() {
});
}
} catch (err) {
console.error("Error saving bookmark:", err);
console.error("Error menyimpan bookmark:", err);
MySwal.fire({
icon: "error",
title: "Kesalahan",
@ -207,31 +204,54 @@ export default function MediaUpdate() {
Media Update
</h2>
{/* Tab */}
{/* 🔸 Tab Urutan */}
<div className="flex justify-center mb-8 bg-white">
<Card className="bg-[#FFFFFF] rounded-xl flex flex-row p-3 gap-2">
<button
onClick={() => setTab("latest")}
onClick={() => setUrutan("latest")}
className={`px-5 py-2 rounded-lg text-sm font-medium ${
tab === "latest" ? "bg-[#C6A455] text-white" : "text-[#C6A455]"
urutan === "latest"
? "bg-[#C6A455] text-white"
: "text-[#C6A455]"
}`}
>
Konten Terbaru
Terbaru
</button>
<button
onClick={() => setTab("popular")}
onClick={() => setUrutan("popular")}
className={`px-5 py-2 rounded-lg text-sm font-medium ${
tab === "popular" ? "bg-[#C6A455] text-white" : "text-[#C6A455]"
urutan === "popular"
? "bg-[#C6A455] text-white"
: "text-[#C6A455]"
}`}
>
Konten Terpopuler
Terpopuler
</button>
</Card>
</div>
{/* Slider */}
{/* 🔸 Tabs Tipe Konten */}
<Tabs value={tipeKonten} onValueChange={(v: any) => setTipeKonten(v)}>
<TabsList className="flex mb-6 pb-2 bg-transparent">
{["image", "video", "text", "audio"].map((tipe) => (
<TabsTrigger
key={tipe}
value={tipe}
className={`px-5 py-2 rounded-lg text-sm font-medium mx-1 border ${
tipeKonten === tipe
? "bg-[#C6A455] text-white border-[#C6A455]"
: "text-[#C6A455] border-[#C6A455]"
}`}
>
{tipe.charAt(0).toUpperCase() + tipe.slice(1)}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{/* 🔸 Konten */}
{loading ? (
<p className="text-center">Loading...</p>
<p className="text-center">Memuat konten...</p>
) : (
<Swiper
modules={[Navigation]}
@ -243,7 +263,7 @@ export default function MediaUpdate() {
1024: { slidesPerView: 4 },
}}
>
{dataToRender.map((item) => (
{dataKonten.map((item) => (
<SwiperSlide key={item.id}>
<div className="rounded-xl shadow-md overflow-hidden bg-white">
<div className="w-full h-[204px] relative">
@ -259,8 +279,8 @@ export default function MediaUpdate() {
<span className="text-xs text-white px-2 py-0.5 rounded bg-blue-600">
{item.category || "Tanpa Kategori"}
</span>
<span className="text-xs font-medium text-[#b3882e]">
{item.label || ""}
<span className="text-xs font-medium text-[#b3882e] capitalize">
{tipeKonten}
</span>
</div>
<p className="text-xs text-gray-500 mb-1">
@ -286,7 +306,9 @@ export default function MediaUpdate() {
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{bookmarkedIds.has(Number(item.id)) ? "Saved" : "Save"}
{bookmarkedIds.has(Number(item.id))
? "Tersimpan"
: "Simpan"}
</Button>
</div>
</div>
@ -296,21 +318,13 @@ export default function MediaUpdate() {
</Swiper>
)}
{/* Lihat lebih banyak */}
{/* 🔸 Tombol Lihat Lebih Banyak */}
<div className="text-center mt-10">
<Link
href={
tab === "latest"
? "https://mediahub.polri.go.id/"
: "https://tribratanews.polri.go.id/"
}
href={`/${tipeKonten}/filter?sortBy=${urutan}`}
className="inline-block border border-[#b3882e] text-[#b3882e] px-6 py-2 rounded-md text-sm font-medium hover:bg-[#b3882e]/10 transition"
>
<Button
size={"lg"}
className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition"
>
Lihat Lebih Banyak
</Button>
Lihat Lebih Banyak
</Link>
</div>
</div>
@ -327,10 +341,14 @@ export default function MediaUpdate() {
// import { Card } from "../ui/card";
// import Link from "next/link";
// import { listData, listArticles } from "@/service/landing/landing";
// import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
// import { getCookiesDecrypt } from "@/lib/utils";
// import { Swiper, SwiperSlide } from "swiper/react";
// import "swiper/css";
// import "swiper/css/navigation";
// import { Navigation } from "swiper/modules";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// // Format tanggal
// function formatTanggal(dateString: string) {
@ -353,7 +371,9 @@ export default function MediaUpdate() {
// export default function MediaUpdate() {
// const [tab, setTab] = useState<"latest" | "popular">("latest");
// const [dataToRender, setDataToRender] = useState<any[]>([]);
// const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
// const [loading, setLoading] = useState(true);
// const MySwal = withReactContent(Swal);
// useEffect(() => {
// fetchData(tab);
@ -363,21 +383,19 @@ export default function MediaUpdate() {
// try {
// setLoading(true);
// // Use new Articles API
// const response = await listArticles(
// 1,
// 20,
// 1, // typeId for images
// 1, // typeId = image
// undefined,
// undefined,
// section === "latest" ? "createdAt" : "viewCount"
// );
// console.log("Media Update Articles API response:", response);
// let articlesData: any[] = [];
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// console.error("Articles API failed, fallback ke old API");
// const fallbackRes = await listData(
// "1",
// "",
@ -389,49 +407,132 @@ export default function MediaUpdate() {
// "",
// ""
// );
// setDataToRender(fallbackRes?.data?.data?.content || []);
// return;
// articlesData = fallbackRes?.data?.data?.content || [];
// } else {
// articlesData = response?.data?.data || [];
// }
// // Handle new API response structure
// const articlesData = response?.data?.data || [];
// // Transform articles data to match old structure for backward compatibility
// // 🔹 Normalisasi struktur data
// const transformedData = articlesData.map((article: any) => ({
// id: article.id,
// title: article.title,
// category: article.categoryName || (article.categories && article.categories[0]?.title) || "Tanpa Kategori",
// category:
// article.categoryName ||
// (article.categories && article.categories[0]?.title) ||
// "Tanpa Kategori",
// createdAt: article.createdAt,
// smallThumbnailLink: article.thumbnailUrl,
// label: article.typeId === 1 ? "Image" : article.typeId === 2 ? "Video" : article.typeId === 3 ? "Text" : article.typeId === 4 ? "Audio" : "",
// ...article
// label:
// article.typeId === 1
// ? "Image"
// : article.typeId === 2
// ? "Video"
// : article.typeId === 3
// ? "Text"
// : article.typeId === 4
// ? "Audio"
// : "",
// ...article,
// }));
// setDataToRender(transformedData);
// // 🔹 Sinkronisasi bookmark
// const roleId = Number(getCookiesDecrypt("urie"));
// if (roleId && !isNaN(roleId)) {
// const savedLocal = localStorage.getItem("bookmarkedIds");
// let localSet = new Set<number>();
// if (savedLocal) {
// localSet = new Set(JSON.parse(savedLocal));
// setBookmarkedIds(localSet);
// }
// 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 merged = new Set([...localSet, ...ids]);
// setBookmarkedIds(merged);
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(merged))
// );
// }
// } catch (err) {
// console.error("Gagal memuat data:", err);
// // Try fallback to old API if new API fails
// try {
// const fallbackRes = await listData(
// "1",
// "",
// "",
// 20,
// 0,
// section === "latest" ? "createdAt" : "clickCount",
// "",
// "",
// ""
// );
// setDataToRender(fallbackRes?.data?.data?.content || []);
// } catch (fallbackError) {
// console.error("Fallback API also failed:", fallbackError);
// }
// } finally {
// setLoading(false);
// }
// }
// // 🔹 Simpan perubahan bookmark ke localStorage
// useEffect(() => {
// if (bookmarkedIds.size > 0) {
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(bookmarkedIds))
// );
// }
// }, [bookmarkedIds]);
// const handleSave = async (id: number) => {
// const roleId = Number(getCookiesDecrypt("urie"));
// if (!roleId || isNaN(roleId)) {
// MySwal.fire({
// icon: "warning",
// title: "Login diperlukan",
// text: "Silakan login terlebih dahulu untuk menyimpan artikel.",
// confirmButtonText: "Login Sekarang",
// confirmButtonColor: "#d33",
// });
// return;
// }
// try {
// const res = await toggleBookmark(id);
// if (res?.error) {
// MySwal.fire({
// icon: "error",
// title: "Gagal",
// text: "Gagal menyimpan artikel.",
// confirmButtonColor: "#d33",
// });
// } else {
// const updated = new Set(bookmarkedIds);
// updated.add(Number(id));
// setBookmarkedIds(updated);
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(updated))
// );
// MySwal.fire({
// icon: "success",
// title: "Berhasil",
// text: "Artikel berhasil disimpan ke bookmark.",
// timer: 1500,
// showConfirmButton: false,
// });
// }
// } catch (err) {
// console.error("Error saving bookmark:", err);
// MySwal.fire({
// icon: "error",
// title: "Kesalahan",
// text: "Terjadi kesalahan saat menyimpan artikel.",
// });
// }
// };
// return (
// <section className="bg-white px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
// <div className="max-w-screen-xl mx-auto">
@ -508,11 +609,17 @@ export default function MediaUpdate() {
// <ThumbsDown className="w-4 h-4 cursor-pointer" />
// </div>
// <Button
// onClick={() => handleSave(item.id)}
// disabled={bookmarkedIds.has(Number(item.id))}
// variant="default"
// size="sm"
// className="text-white bg-blue-600 rounded px-4"
// className={`rounded px-4 ${
// bookmarkedIds.has(Number(item.id))
// ? "bg-gray-400 cursor-not-allowed text-white"
// : "bg-blue-600 text-white hover:bg-blue-700"
// }`}
// >
// Save
// {bookmarkedIds.has(Number(item.id)) ? "Saved" : "Save"}
// </Button>
// </div>
// </div>

View File

@ -0,0 +1,112 @@
import { Button } from '@/components/ui/button';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { useSearchParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
interface DataTablePaginationProps {
table: Table<any>;
totalPage: number; // Total jumlah halaman
totalData: number; // Total jumlah data
visiblePageCount?: number; // Jumlah halaman yang ditampilkan (default 5)
}
const LandingPagination = ({
table,
totalPage,
totalData,
visiblePageCount = 5,
}: DataTablePaginationProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [currentPageIndex, setCurrentPageIndex] = useState<number>(1);
useEffect(() => {
const pageFromUrl = searchParams?.get('page');
if (pageFromUrl) {
const pageIndex = Math.min(Math.max(1, Number(pageFromUrl)), totalPage);
setCurrentPageIndex(pageIndex);
table.setPageIndex(pageIndex - 1); // Sinkronisasi tabel dengan URL
}
}, [searchParams, totalPage, table]);
const handlePageChange = (pageIndex: number) => {
const clampedPageIndex = Math.min(Math.max(1, pageIndex), totalPage);
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('page', clampedPageIndex.toString());
router.push(`${window.location.pathname}?${searchParams.toString()}`);
setCurrentPageIndex(clampedPageIndex);
table.setPageIndex(clampedPageIndex - 1); // Perbarui tabel dengan index berbasis 0
};
const generatePageNumbers = () => {
const halfVisible = Math.floor(visiblePageCount / 2);
let startPage = Math.max(1, currentPageIndex - halfVisible);
let endPage = Math.min(totalPage, startPage + visiblePageCount - 1);
if (endPage - startPage + 1 < visiblePageCount) {
startPage = Math.max(1, endPage - visiblePageCount + 1);
}
return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
};
return (
<div className="flex items-center justify-center space-y-4 mt-8 md:space-y-0">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(1)}
disabled={currentPageIndex === 1}
className="w-8 h-8"
>
<ChevronsLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPageIndex-1)}
disabled={currentPageIndex === 1}
className="w-8 h-8"
>
<ChevronLeft className="w-4 h-4" />
</Button>
{generatePageNumbers().map((pageIndex) => (
<Button
key={pageIndex}
onClick={() => handlePageChange(pageIndex)}
size="icon"
className="w-8 h-8"
variant={currentPageIndex === pageIndex ? 'default' : 'outline'}
>
{pageIndex}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPageIndex+1)}
disabled={currentPageIndex === totalPage}
className="w-8 h-8"
>
<ChevronRight className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(totalPage)}
disabled={currentPageIndex === totalPage}
className="w-8 h-8"
>
<ChevronsRight className="w-4 h-4" />
</Button>
</div>
</div>
);
};
export default LandingPagination;

View File

@ -1,6 +1,6 @@
"use client";
import Image from "next/image";
import { Calendar, Clock, Eye } from "lucide-react";
import { Calendar, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useState, useEffect } from "react";
@ -15,10 +15,10 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
import { getDetail } from "@/service/landing/landing";
import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton";
import { getArticleDetail } from "@/service/content/content";
export default function ImageDetail({ id }: { id: string }) {
const [copied, setCopied] = useState(false);
@ -28,21 +28,22 @@ export default function ImageDetail({ id }: { id: string }) {
const [selectedImage, setSelectedImage] = useState(0);
const [isLoading, setIsLoading] = useState<any>(true);
// animasi skeleton loading
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
// salin link ke clipboard
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);
}
};
@ -59,82 +60,81 @@ export default function ImageDetail({ id }: { id: string }) {
</div>
);
// 🔹 Ambil data detail dari API
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);
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,
...file
// 1⃣ Coba ambil dari API baru /articles/{id}
const res = await getArticleDetail(id);
console.log("Response dari /articles/{id}:", res);
// if (res?.error || !res?.data?.data) {
// console.warn("Gagal ambil dari API baru, coba fallback ke API lama");
// const fallback = await getArticleDetail(id);
// setData(fallback?.data?.data);
// return;
// }
// 2⃣ Transformasi struktur agar kompatibel dengan UI lama
const article = res?.data?.data;
const mappedData = {
id: article.id,
title: article.title,
description: article.description,
createdAt: article.createdAt,
clickCount: article.viewCount,
creatorGroupLevelName: article.createdByName || "Unknown",
thumbnailUrl: article.thumbnailUrl || "", // ✅ tambahkan ini
uploadedBy: {
publisher: article.createdByName || "MABES POLRI",
},
files:
article.files?.map((f: any) => ({
id: f.id,
url: f.file_url,
fileName: f.file_name,
filePath: f.file_path,
fileThumbnail: f.file_thumbnail,
fileAlt: f.file_alt,
widthPixel: f.width_pixel,
heightPixel: f.height_pixel,
size: f.size,
downloadCount: f.download_count,
createdAt: f.created_at,
updatedAt: f.updated_at,
})) || [],
};
};
console.log("transformedData : ", transformedData.files);
setData(transformedData);
}
} catch (error) {
console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
setData(mappedData);
} catch (err) {
console.error("Gagal ambil detail artikel:", err);
// fallback ke API lama
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
const fallback = await getDetail(id);
setData(fallback?.data?.data);
} catch (err2) {
console.error("Fallback API juga gagal:", err2);
}
} finally {
setLoading(false);
}
};
if (id) fetchDetail();
}, [id]);
// 🔹 UI Loading
if (loading) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
<p>Loading...</p>
<p>Memuat data...</p>
</div>
);
}
// 🔹 Jika data kosong
if (!data) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
@ -143,8 +143,10 @@ export default function ImageDetail({ id }: { id: string }) {
);
}
// 🔹 Render konten utama
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Gambar Utama */}
{isLoading ? (
<div className="relative">
<Skeleton className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-[900px]" />
@ -157,16 +159,18 @@ export default function ImageDetail({ id }: { id: string }) {
)}`}
width={2560}
height={1440}
src={data?.files[selectedImage]?.url || "/nodata.png"}
src={
data?.files?.[selectedImage]?.url ||
data?.thumbnailUrl || // ✅ fallback ke thumbnailUrl
"/nodata.png"
}
alt="Main"
className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full object-contain"
/>
<div className="absolute top-4 right-4"></div>
</div>
)}
{/* Gambar bawah Kecil */}
{/* Thumbnail bawah */}
{isLoading ? (
<div className="py-4 flex flex-row gap-3">
<Skeleton className="rounded-lg w-[120px] h-[90px]" />
@ -174,22 +178,6 @@ export default function ImageDetail({ id }: { id: string }) {
<Skeleton className="rounded-lg w-[120px] h-[90px]" />
</div>
) : (
// <div className="py-4 flex flex-row gap-3">
// {detailDataImage?.files?.map((file: any, index: number) => (
// <a onClick={() => setSelectedImage(index)} key={file?.id}>
// <Image
// placeholder={`data:image/svg+xml;base64,${toBase64(
// shimmer(700, 475)
// )}`}
// width={1920}
// height={1080}
// alt="image-small"
// src={file?.url}
// className="w-[120px] h-[90px] object-cover rounded-md cursor-pointer hover:ring-2 hover:ring-red-600"
// />
// </a>
// ))}
// </div>
<div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
{data?.files?.map((file: any, index: number) => (
<a onClick={() => setSelectedImage(index)} key={file?.id}>
@ -210,11 +198,12 @@ export default function ImageDetail({ id }: { id: string }) {
</div>
)}
{/* Informasi artikel */}
<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">
{/* <span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
</span>
</span> */}
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
{new Date(data.createdAt)
@ -228,92 +217,28 @@ export default function ImageDetail({ id }: { id: string }) {
timeZone: "Asia/Jakarta",
})
.replace(".", ":")}{" "}
WIB
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}
{data.viewCount || 0}
</span>
<span className="text-black">
{" "}
Creator: {data.creatorGroupLevelName}
</span>
</div>
</div>
{/* Konten artikel */}
<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 md:flex-row justify-center gap-4 my-20">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
@ -341,17 +266,7 @@ export default function ImageDetail({ 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>
@ -362,3 +277,368 @@ export default function ImageDetail({ 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";
// import VideoPlayer from "@/utils/video-player";
// import { toBase64, shimmer } from "@/utils/globals";
// import { Skeleton } from "@/components/ui/skeleton";
// export default function ImageDetail({ 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 [selectedImage, setSelectedImage] = 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);
// 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,
// ...file
// })) || [],
// };
// console.log("transformedData : ", transformedData.files);
// setData(transformedData);
// }
// } 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);
// } 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">
// {isLoading ? (
// <div className="relative">
// <Skeleton className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-[900px]" />
// </div>
// ) : (
// <div className="relative">
// <Image
// placeholder={`data:image/svg+xml;base64,${toBase64(
// shimmer(700, 475)
// )}`}
// width={2560}
// height={1440}
// src={data?.files[selectedImage]?.url || "/nodata.png"}
// alt="Main"
// className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full object-contain"
// />
// <div className="absolute top-4 right-4"></div>
// </div>
// )}
// {/* Gambar bawah Kecil */}
// {isLoading ? (
// <div className="py-4 flex flex-row gap-3">
// <Skeleton className="rounded-lg w-[120px] h-[90px]" />
// <Skeleton className="rounded-lg w-[120px] h-[90px]" />
// <Skeleton className="rounded-lg w-[120px] h-[90px]" />
// </div>
// ) : (
// // <div className="py-4 flex flex-row gap-3">
// // {detailDataImage?.files?.map((file: any, index: number) => (
// // <a onClick={() => setSelectedImage(index)} key={file?.id}>
// // <Image
// // placeholder={`data:image/svg+xml;base64,${toBase64(
// // shimmer(700, 475)
// // )}`}
// // width={1920}
// // height={1080}
// // alt="image-small"
// // src={file?.url}
// // className="w-[120px] h-[90px] object-cover rounded-md cursor-pointer hover:ring-2 hover:ring-red-600"
// // />
// // </a>
// // ))}
// // </div>
// <div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
// {data?.files?.map((file: any, index: number) => (
// <a onClick={() => setSelectedImage(index)} key={file?.id}>
// <Image
// placeholder={`data:image/svg+xml;base64,${toBase64(
// shimmer(700, 475)
// )}`}
// width={1920}
// height={1080}
// alt="image-small"
// src={file?.url}
// className={`w-[120px] h-[90px] object-cover rounded-md cursor-pointer hover:ring-2 ${
// selectedImage === index ? "ring-2 ring-red-600" : ""
// }`}
// />
// </a>
// ))}
// </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

@ -578,7 +578,7 @@ export interface ArticleDetailResponse {
}
// Function to fetch article detail
export async function getArticleDetail(id: number) {
export async function getArticleDetail(id: string) {
const url = `articles/${id}`;
return await httpGetInterceptor(url);
}

View File

@ -116,6 +116,7 @@ export async function getDetail(slug: string) {
return await httpGetInterceptor(`media/public?slug=${slug}&state=mabes`);
}
export async function getDetailMetaData(slug: string) {
return await httpGetInterceptorForMetadata(
`media/public?slug=${slug}&state=mabes&isSummary=true`