web-humas-fe/components/main/detail/list-news.tsx

576 lines
17 KiB
TypeScript

"use client";
import {
Autocomplete,
AutocompleteItem,
BreadcrumbItem,
Breadcrumbs,
Button,
DatePicker,
Image,
Input,
Listbox,
ListboxItem,
Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
} from "@heroui/react";
import {
CalendarIcon,
Calender,
ChevronLeftIcon,
ChevronRightIcon,
ClockIcon,
EyeFilledIcon,
SearchIcon,
TimesIcon,
UserIcon,
} from "../../icons";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import {
getArticleByCategoryLanding,
getListArticle,
} from "@/services/article";
import {
convertDateFormatNoTimeV2,
formatMonthString,
getUnixTimestamp,
htmlToString,
textEllipsis,
} from "@/utils/global";
import {
useParams,
usePathname,
useRouter,
useSearchParams,
} from "next/navigation";
import { close, loading } from "@/config/swal";
import { format } from "date-fns";
import { getCategoryById } from "@/services/master-categories";
import AsyncSelect from "react-select/async";
import { data } from "autoprefixer";
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export default function ListNews() {
const [article, setArticle] = useState<any>([]);
const [page, setPage] = useState(1);
const router = useRouter();
const pathname = usePathname();
const [totalPage, setTotalPage] = useState(1);
const topRef = useRef<HTMLDivElement>(null);
const params = useParams();
const category = params?.name;
const searchParams = useSearchParams();
const categoryIds = searchParams.get("category_id");
const [categories, setCategories] = useState<any>([]);
const [searchValue, setSearchValue] = useState(
searchParams.get("search") || "",
);
const [categorySearch, setCategorySearch] = useState("");
const [selectedCategoryId, setSelectedCategoryId] = useState<any>([]);
const today = new Date();
const [year, setYear] = useState(today.getFullYear());
const [selectedMonth, setSelectedMonth] = useState<number | null>(
searchParams.get("month") ? Number(searchParams.get("month")) - 1 : null,
);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [poldaCategId, setPoldaCategId] = useState(0);
const isPoldaPage = pathname.includes("/news/polda/");
const [poldaCategoryOption, setPoldaCategoryOption] = useState<any | null>(
null,
);
const requestIdRef = useRef(0);
const handleMonthClick = (monthIndex: number) => {
setSelectedMonth(monthIndex);
setSelectedDate(new Date(year, monthIndex, 1));
};
useEffect(() => {
if (isPoldaPage) getPoldaCategId();
}, [params, isPoldaPage]);
const getPoldaCategId = async () => {
const poldaNow = category as string;
const dataNow = await getCategory(poldaNow.split("-").join(" "));
if (dataNow?.length > 0) {
const poldaObj = dataNow[0];
setPoldaCategId(poldaObj.id);
const option = setupCategory([poldaObj])[0];
setPoldaCategoryOption(option);
setSelectedCategoryId((prev: any[]) => {
const already = prev.some((x) => x.id === option.id);
if (already) return prev;
return [option, ...prev];
});
}
};
useEffect(() => {
const search = searchParams.get("search") || "";
const category = searchParams.get("category_id") || "";
const month = searchParams.get("month");
const yearQ = searchParams.get("year");
const pageQ = searchParams.get("page");
setSearchValue(search);
if (pageQ) setPage(Number(pageQ));
else setPage(1);
if (yearQ) setYear(Number(yearQ));
if (month && yearQ) {
const m = Number(month) - 1;
setSelectedMonth(m);
setSelectedDate(new Date(Number(yearQ), m, 1));
} else {
setSelectedMonth(null);
setSelectedDate(null);
}
if (category && category !== "") {
getCategoryFromQueries(category.split(","));
} else {
setSelectedCategoryId([]);
getArticle({
title: search,
category: [],
month: month ? Number(month) : null,
year: yearQ ? Number(yearQ) : null,
page: pageQ ? Number(pageQ) : 1,
});
}
}, [searchParams, poldaCategId]);
const getCategoryFromQueries = async (category: string[]) => {
const temp = [];
for (const element of category) {
const res = await getCategoryById(Number(element));
if (res?.data?.data) {
temp.push(res?.data?.data);
}
}
const setup = setupCategory(temp);
setSelectedCategoryId(setup);
const search = searchParams.get("search") || "";
const month = searchParams.get("month");
const yearQ = searchParams.get("year");
const pageQ = searchParams.get("page");
await getArticle({
title: search,
category: setup,
month: month ? Number(month) : null,
year: yearQ ? Number(yearQ) : null,
page: pageQ ? Number(pageQ) : 1,
});
};
// useEffect(() => {
// getArticle();
// }, [page, searchParams]);
async function getArticle(props?: {
title?: string;
category?: any[];
month?: number | null;
year?: number | null;
page?: number;
}) {
requestIdRef.current += 1;
const currentRequestId = requestIdRef.current;
loading();
const usedPage = props?.page || page;
const usedSearch = props?.title ?? searchValue ?? "";
const baseCategories =
props?.category && props.category.length > 0
? props.category
: selectedCategoryId;
let finalCategories = baseCategories;
if (isPoldaPage && poldaCategId) {
const hasPolda = baseCategories.some((x: any) => x.id === poldaCategId);
if (!hasPolda) {
finalCategories = [
...(poldaCategoryOption ? [poldaCategoryOption] : []),
...baseCategories,
];
}
}
const usedCategoryIds =
finalCategories.length > 0
? finalCategories.map((val: any) => val.id).join(",")
: "";
const usedMonth =
props?.month ?? (selectedDate ? selectedDate.getMonth() + 1 : null);
const usedYear =
props?.year ?? (selectedDate ? selectedDate.getFullYear() : null);
const req = {
page: usedPage,
search: usedSearch,
limit: "9",
isPublish: true,
sort: "desc",
categorySlug: pathname.includes("satker") ? String(category) : "",
categoryIds: usedCategoryIds,
startDate:
usedMonth && usedYear
? convertDateFormatNoTimeV2(new Date(usedYear, usedMonth - 1, 1))
: "",
endDate:
usedMonth && usedYear
? convertDateFormatNoTimeV2(new Date(usedYear, usedMonth, 0))
: "",
timeStamp: getUnixTimestamp(),
};
try {
const response = await getListArticle(req);
// ❗ GUARD: hanya request terakhir yang boleh set state
if (currentRequestId !== requestIdRef.current) return;
setArticle(response?.data?.data);
setTotalPage(response?.data?.meta?.totalPage);
} finally {
if (currentRequestId === requestIdRef.current) {
close();
}
}
}
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
const getCategory = async (search?: string) => {
const res = await getArticleByCategoryLanding({
// limit: debouncedValue === "" ? "5" : "",
// title: debouncedValue,
limit: !search || search === "" ? "5" : "",
title: search ? search : "",
timeStamp: getUnixTimestamp(),
});
if (res?.data?.data) {
setCategories(res?.data?.data);
return res?.data?.data;
}
return [];
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
return temp;
};
const loadOptions = useCallback(
(inputValue: string, callback: (options: any) => void) => {
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
debounceTimeout.current = setTimeout(async () => {
try {
const data = await getCategory(inputValue);
callback(setupCategory(data));
} catch (error) {
callback([]);
}
}, 1500);
},
[],
);
const updateQuery = (newParams: Record<string, any>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(newParams).forEach(([key, value]) => {
if (
value === undefined ||
value === null ||
value === "" ||
(Array.isArray(value) && value.length === 0)
) {
params.delete(key);
} else {
params.set(key, String(value));
}
});
router.push(`${pathname}?${params.toString()}`);
};
return (
<div className="bg-white border-b-1" ref={topRef}>
<div className="text-black py-5 px-3 lg:w-[75vw] mx-auto bg-white">
<div className="flex flex-row gap-4">
<Link href="/" className="text-black font-semibold">
Beranda
</Link>
<ChevronRightIcon />
<p className="text-black">Berita</p>
</div>
<div className="w-full flex justify-center">
<div className="py-5 lg:py-10 lg:mx-auto flex flex-col lg:flex-row gap-2 items-end grow-0 mx-auto">
<Input
aria-label="Judul"
className="w-full lg:w-[300px]"
classNames={{
inputWrapper: "bg-white hover:!bg-gray-100 border-1 rounded-md",
input: "text-sm !text-black outline-none",
}}
// onKeyDown={(event) => {
// if (event.key === "Enter") {
// router.push(pathname + `?search=${searchValue}`);
// getArticle();
// }
// }}
labelPlacement="outside"
placeholder="Judul..."
value={searchValue}
onValueChange={setSearchValue}
type="search"
/>
<AsyncSelect
isMulti
loadOptions={loadOptions}
defaultOptions
placeholder="Kategori"
className="z-50 min-w-[300px] max-w-[600px]"
classNames={{
control: () =>
"border border-gray-300 border-1 rounded-xl min-w-[300px] max-w-[600px]",
menu: () => "z-50",
}}
value={selectedCategoryId}
onChange={(val: any) => {
const nextValue = val || [];
if (!pathname.includes("/news/polda/")) {
setSelectedCategoryId(nextValue);
return;
}
if (poldaCategoryOption) {
const hasPolda = nextValue.some(
(x: any) => x.id === poldaCategoryOption.id,
);
if (!hasPolda) {
setSelectedCategoryId([poldaCategoryOption, ...nextValue]);
return;
}
}
setSelectedCategoryId(nextValue);
}}
/>
<div className="flex flex-row items-center border-1 h-[40px] w-full lg:w-[200px] rounded-md px-2">
<Popover placement="bottom" showArrow={true} className="w-full">
<PopoverTrigger>
<a className="px-2 py-1 text-sm w-[220px]">
{" "}
{selectedDate
? format(selectedDate, "MMMM yyyy")
: "Pilih Bulan"}
</a>
</PopoverTrigger>
<PopoverContent className="p-4 w-[200px]">
<div className="flex items-center justify-between mb-2 px-1 w-full">
<button
className="text-gray-500 hover:text-black"
onClick={() => setYear((prev) => prev - 1)}
>
<ChevronLeftIcon />
</button>
<span className="font-semibold text-center">{year}</span>
<button
className="text-gray-500 hover:text-black"
onClick={() => setYear((prev) => prev + 1)}
>
<ChevronRightIcon />
</button>
</div>
<div className="grid grid-cols-3 gap-2 w-full">
{months.map((month, idx) => (
<button
key={idx}
onClick={() => handleMonthClick(idx)}
className={`py-1 rounded-md text-sm transition-colors ${
selectedDate &&
selectedDate.getMonth() === idx &&
selectedDate.getFullYear() === year
? "bg-blue-500 text-white"
: "hover:bg-gray-200 text-gray-700"
}`}
>
{month}
</button>
))}
</div>
</PopoverContent>
</Popover>{" "}
{selectedDate && (
<a
className="cursor-pointer w-[20px]"
onClick={() => {
setSelectedDate(null);
setSelectedMonth(null);
updateQuery({ month: "", year: "", page: 1 });
}}
>
<TimesIcon size={20} />
</a>
)}
</div>
{/* <Button
onPress={() => getArticle()}
className="bg-red-600 text-white w-[80px] rounded-md"
>
Cari
</Button> */}
<Button
onPress={() => {
updateQuery({
search: searchValue,
category_id:
selectedCategoryId.length > 0
? selectedCategoryId.map((v: any) => v.id).join(",")
: "",
month: selectedDate ? selectedDate.getMonth() + 1 : "",
year: selectedDate ? selectedDate.getFullYear() : "",
page: 1, // reset page setiap cari
});
}}
className="bg-red-600 text-white w-[80px] rounded-md"
>
Cari
</Button>
{/* </Link> */}
</div>
</div>
{article?.length < 1 || !article ? (
<div className="flex justify-center items-center">Tidak ada Data</div>
) : (
<>
<section
id="content"
className=" grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-7"
>
{article?.map((news: any) => (
<Link
href={`/news/detail/${news?.id}-${news?.slug}`}
key={news?.id}
>
<div className="">
<Image
src={
news.thumbnailUrl == ""
? "/no-image.jpg"
: news.thumbnailUrl
}
width={1920}
alt="thumbnail"
className="rounded-b-none h-[27vh] w-full object-cover"
/>
</div>
<div className="p-2 lg:p-5 bg-[#f0f0f0] rounded-b-md">
<div className="font-semibold text-lg">{news?.title}</div>
<div className="flex flex-row items-center py-1 text-[10px] gap-2">
<div className="flex flex-row items-center gap-1">
<CalendarIcon size={18} />
<p>{formatMonthString(news?.createdAt)}</p>
</div>
<div className="flex flex-row items-center">
<ClockIcon size={18} />
<p>{`${new Date(news?.createdAt)
.getHours()
.toString()
.padStart(2, "0")}:${new Date(news?.createdAt)
.getMinutes()
.toString()
.padStart(2, "0")}`}</p>
</div>
<div className="flex flex-row items-center gap-1">
<UserIcon size={14} />
<p>{news?.createdByName}</p>
</div>
</div>
<div className="text-sm">
{textEllipsis(htmlToString(news?.description), 165)}
</div>
</div>
</Link>
))}
</section>
<div className="flex justify-center mt-5">
{/* <Pagination
page={page}
total={totalPage}
onChange={setPage}
classNames={{
item: "w-fit px-3",
cursor: "w-fit px-3",
}}
/> */}
<Pagination
page={page}
total={totalPage}
onChange={(p) => {
setPage(p);
updateQuery({ page: p });
}}
classNames={{
item: "w-fit px-3",
cursor: "w-fit px-3",
}}
/>
</div>
</>
)}
</div>
</div>
);
}