211 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|