satupeta-main/app/(modules)/news/page.tsx
2026-01-27 09:31:12 +07:00

211 lines
7.3 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { PaginatedResponse } from "@/shared/types/api-response";
import { News } from "@/shared/types/news";
import newsApi from "@/shared/services/news";
import DOMPurify from "dompurify";
import { getFileUrl } from "@/shared/utils/file";
import Image from "next/image";
import Link from "next/link";
import { NewspaperIcon } from "lucide-react";
export default function NewsPage() {
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [filter] = useState('["is_active=true"]');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const limit = 10;
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
const { data, isLoading, isError } = useQuery<PaginatedResponse<News[]>>({
queryKey: ["news", page, debouncedSearchTerm, filter],
queryFn: () =>
newsApi.getNewsList({
search: debouncedSearchTerm,
filter: filter,
limit: limit,
offset: (page - 1) * limit,
}),
});
// Debounce search input
useEffect(() => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
setPage(1); // Reset to first page when search term changes
}, 500); // 500ms delay after typing stops
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, [searchTerm]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// Immediately trigger search on form submit
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
setDebouncedSearchTerm(searchTerm);
setPage(1);
};
if (isError) {
return (
<div className="flex h-full w-screen items-center justify-center">
Error loading news
</div>
);
}
return (
<div className="min-h-screen w-full p-6">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-10 text-5xl text-slate-700">Berita dan Pengumuman</h2>
<p className="mb-6 max-w-3xl text-xl text-slate-500">
Temukan informasi yang bersifat faktual dengan sumber data terpercaya
yang disajikan dalam bentuk teks.
</p>
<div>
<form
onSubmit={handleSearch}
className="mb-4 flex flex-col gap-4 md:flex-row"
>
<div className="flex-1">
<input
type="text"
id="search"
placeholder="Cari Berita dan Pengumuman..."
className="w-full rounded-md border border-gray-300 p-2"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="self-end">
<button
type="submit"
className="bg-primary rounded-md px-4 py-2 text-white"
>
Cari
</button>
</div>
</form>
</div>
<div className="mb-4 flex items-center justify-between">
<p className="text-slate-500">
{data?.total || 0} Berita dan Pengumuman ditemukan
</p>
</div>
{isLoading && (
<div className="flex items-center justify-center py-10">
<div className="border-primary h-12 w-12 animate-spin rounded-full border-t-2 border-b-2"></div>
</div>
)}
{!isLoading && data?.items && data?.items?.length > 0 && (
<>
<div className="flex flex-col space-y-4">
{data?.items.map((newsItem) => (
<Link href={`/news/${newsItem.id}`} key={newsItem.id}>
<div className="flex cursor-pointer gap-4 rounded-lg bg-white p-6 shadow transition-shadow hover:shadow-md">
{newsItem.thumbnail && (
<div className="h-24 w-24 flex-shrink-0">
<Image
src={getFileUrl(newsItem.thumbnail)}
alt={newsItem.name}
className="h-full w-full rounded-md object-cover"
width={300}
height={300}
/>
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="mb-2 truncate text-xl font-semibold text-gray-900">
{newsItem.name}
</h3>
<div
className="line-clamp-3 text-gray-600"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(newsItem.description),
}}
/>
</div>
</div>
</Link>
))}
</div>
{data?.total && data.total > limit && (
<div className="mt-8 flex justify-center">
<nav className="inline-flex rounded-md shadow">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="rounded-l-md border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
{Array.from(
{ length: Math.ceil(data.total / limit) },
(_, i) => i + 1,
).map((pageNum) => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`border-t border-b border-gray-300 px-4 py-2 ${
page === pageNum
? "border-blue-500 bg-blue-50 text-blue-600"
: "bg-white text-gray-700 hover:bg-gray-50"
}`}
>
{pageNum}
</button>
))}
<button
onClick={() =>
setPage((p) =>
Math.min(Math.ceil(data.total / limit), p + 1),
)
}
disabled={page === Math.ceil(data.total / limit)}
className="rounded-r-md border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
)}
</>
)}
{!isLoading && data?.items.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-center text-slate-500">
<NewspaperIcon width={150} height={150} className="mb-6" />
<h3 className="mb-2 text-xl font-semibold">
Tidak ada berita ditemukan
</h3>
<p className="max-w-md text-gray-500">
Coba kata kunci pencarian lain atau periksa ejaan Anda.
</p>
</div>
)}
</div>
</div>
);
}