576 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|