feat: add list article

This commit is contained in:
hanif salafi 2025-10-12 21:57:46 +07:00
parent 95a24644e8
commit 760aa93b2d
6 changed files with 2050 additions and 30 deletions

View File

@ -0,0 +1,501 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ThumbsUp, ThumbsDown, Search, Filter, Calendar, Tag } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { listArticles } from "@/service/landing/landing";
import { getArticleCategories } from "@/service/categories/article-categories";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// Format tanggal
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
new Date(dateString)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(/\./g, ":") + " WIB"
);
}
// Function to get link based on typeId
function getLink(item: any) {
switch (item?.typeId) {
case 1:
return `/content/image/detail/${item?.id}`;
case 2:
return `/content/video/detail/${item?.id}`;
case 3:
return `/content/text/detail/${item?.id}`;
case 4:
return `/content/audio/detail/${item?.id}`;
default:
return "#";
}
}
export default function ArticleListPage() {
const [articles, setArticles] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [selectedType, setSelectedType] = useState<string>("4");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalData, setTotalData] = useState(0);
const router = useRouter();
const searchParams = useSearchParams();
const MySwal = withReactContent(Swal);
const itemsPerPage = 6;
// Load categories on component mount
useEffect(() => {
loadCategories();
}, []);
// Load articles when filters change
useEffect(() => {
loadArticles();
}, [currentPage, selectedCategory, selectedType, searchTerm, startDate, endDate]);
// Sync bookmarks
useEffect(() => {
syncBookmarks();
}, []);
async function loadCategories() {
try {
const response = await getArticleCategories();
if (response?.data?.data) {
setCategories(response.data.data);
}
} catch (error) {
console.error("Error loading categories:", error);
}
}
async function loadArticles() {
try {
setLoading(true);
const categoryId = selectedCategory === "all" ? undefined : selectedCategory;
const typeId = selectedType === "all" ? undefined : parseInt(selectedType);
const response = await listArticles(
currentPage,
itemsPerPage,
typeId,
searchTerm || undefined,
categoryId,
"createdAt"
);
if (response?.data?.data) {
const articlesData = response.data.data;
setArticles(articlesData);
setTotalPages(response.data.meta.totalPage || 1);
setTotalData(response.data.meta.count || 0);
}
} catch (error) {
console.error("Error loading articles:", error);
} finally {
setLoading(false);
}
}
async function syncBookmarks() {
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
try {
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 (error) {
console.error("Error syncing bookmarks:", error);
}
}
}
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.",
});
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
loadArticles();
};
const handleFilterChange = () => {
setCurrentPage(1);
loadArticles();
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const getTypeLabel = (typeId: number) => {
switch (typeId) {
case 1: return "📸 Image";
case 2: return "🎬 Video";
case 3: return "📝 Text";
case 4: return "🎵 Audio";
default: return "📄 Content";
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-2 lg:px-4">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filter Sidebar */}
<div className="lg:w-1/4">
<Card className="p-6 sticky top-4">
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-5 h-5 text-blue-600" />
<h3 className="text-lg font-semibold">Filter Artikel</h3>
</div>
{/* Search */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Cari Artikel</label>
<form onSubmit={handleSearch} className="flex gap-2">
<Input
type="text"
placeholder="Masukkan kata kunci..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1"
/>
<Button type="submit" size="sm">
<Search className="w-4 h-4" />
</Button>
</form>
</div>
{/* Category Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Tag className="w-4 h-4" />
Kategori
</label>
<select
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Kategori</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.title}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Tipe Konten</label>
<select
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Tipe</option>
<option value="1">📸 Image</option>
<option value="2">🎬 Video</option>
<option value="3">📝 Text</option>
<option value="4">🎵 Audio</option>
</select>
</div>
{/* Date Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Rentang Tanggal
</label>
<div className="space-y-2">
<Input
type="date"
placeholder="Tanggal Mulai"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
handleFilterChange();
}}
/>
<Input
type="date"
placeholder="Tanggal Akhir"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
handleFilterChange();
}}
/>
</div>
</div>
{/* Clear Filters */}
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setSelectedCategory("all");
setSelectedType("all");
setStartDate("");
setEndDate("");
setCurrentPage(1);
}}
className="w-full"
>
Reset Filter
</Button>
</div>
</Card>
</div>
{/* Main Content */}
<div className="lg:w-3/4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Daftar Artikel</h1>
<p className="text-gray-600">
Menampilkan {totalData} artikel
{searchTerm && ` untuk "${searchTerm}"`}
{selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`}
</p>
</div>
{/* Articles Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-3 bg-gray-200 rounded w-full"></div>
</div>
</Card>
))}
</div>
) : articles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{articles.map((article) => (
<Card key={article.id} className="overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="w-full h-48 relative">
<Link href={getLink(article)}>
<Image
src={article.thumbnailUrl || "/placeholder.png"}
alt={article.title || "No Title"}
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/>
</Link>
</div>
<div className="p-4">
<div className="flex gap-2 mb-2">
<Badge color="primary" className="text-xs">
{article.clientName}
</Badge>
<Badge color="secondary" className="text-xs">
{article.categoryName || "Tanpa Kategori"}
</Badge>
</div>
<p className="text-xs text-gray-500 mb-2">
{formatTanggal(article.createdAt)}
</p>
<Link href={getLink(article)}>
<h3 className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{article.title}
</h3>
</Link>
{/* <p className="text-xs text-gray-600 mb-4 line-clamp-2">
{article.description || article.content || ""}
</p> */}
<div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer hover:text-blue-600" />
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
</div>
<Button
onClick={() => handleSave(article.id)}
disabled={bookmarkedIds.has(Number(article.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(article.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{bookmarkedIds.has(Number(article.id)) ? "Saved" : "Save"}
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card className="p-8 text-center">
<div className="text-gray-500">
<p className="text-lg mb-2">Tidak ada artikel ditemukan</p>
<p className="text-sm">Coba ubah filter atau kata kunci pencarian</p>
</div>
</Card>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNumber = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
if (pageNumber > totalPages) return null;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
onClick={() => handlePageChange(pageNumber)}
isActive={currentPage === pageNumber}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,501 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ThumbsUp, ThumbsDown, Search, Filter, Calendar, Tag } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { listArticles } from "@/service/landing/landing";
import { getArticleCategories } from "@/service/categories/article-categories";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// Format tanggal
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
new Date(dateString)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(/\./g, ":") + " WIB"
);
}
// Function to get link based on typeId
function getLink(item: any) {
switch (item?.typeId) {
case 1:
return `/content/image/detail/${item?.id}`;
case 2:
return `/content/video/detail/${item?.id}`;
case 3:
return `/content/text/detail/${item?.id}`;
case 4:
return `/content/audio/detail/${item?.id}`;
default:
return "#";
}
}
export default function ArticleListPage() {
const [articles, setArticles] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [selectedType, setSelectedType] = useState<string>("all");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalData, setTotalData] = useState(0);
const router = useRouter();
const searchParams = useSearchParams();
const MySwal = withReactContent(Swal);
const itemsPerPage = 6;
// Load categories on component mount
useEffect(() => {
loadCategories();
}, []);
// Load articles when filters change
useEffect(() => {
loadArticles();
}, [currentPage, selectedCategory, selectedType, searchTerm, startDate, endDate]);
// Sync bookmarks
useEffect(() => {
syncBookmarks();
}, []);
async function loadCategories() {
try {
const response = await getArticleCategories();
if (response?.data?.data) {
setCategories(response.data.data);
}
} catch (error) {
console.error("Error loading categories:", error);
}
}
async function loadArticles() {
try {
setLoading(true);
const categoryId = selectedCategory === "all" ? undefined : selectedCategory;
const typeId = selectedType === "all" ? undefined : parseInt(selectedType);
const response = await listArticles(
currentPage,
itemsPerPage,
typeId,
searchTerm || undefined,
categoryId,
"createdAt"
);
if (response?.data?.data) {
const articlesData = response.data.data;
setArticles(articlesData);
setTotalPages(response.data.meta.totalPage || 1);
setTotalData(response.data.meta.count || 0);
}
} catch (error) {
console.error("Error loading articles:", error);
} finally {
setLoading(false);
}
}
async function syncBookmarks() {
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
try {
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 (error) {
console.error("Error syncing bookmarks:", error);
}
}
}
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.",
});
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
loadArticles();
};
const handleFilterChange = () => {
setCurrentPage(1);
loadArticles();
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const getTypeLabel = (typeId: number) => {
switch (typeId) {
case 1: return "📸 Image";
case 2: return "🎬 Video";
case 3: return "📝 Text";
case 4: return "🎵 Audio";
default: return "📄 Content";
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-2 lg:px-4">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filter Sidebar */}
<div className="lg:w-1/4">
<Card className="p-6 sticky top-4">
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-5 h-5 text-blue-600" />
<h3 className="text-lg font-semibold">Filter Artikel</h3>
</div>
{/* Search */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Cari Artikel</label>
<form onSubmit={handleSearch} className="flex gap-2">
<Input
type="text"
placeholder="Masukkan kata kunci..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1"
/>
<Button type="submit" size="sm">
<Search className="w-4 h-4" />
</Button>
</form>
</div>
{/* Category Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Tag className="w-4 h-4" />
Kategori
</label>
<select
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Kategori</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.title}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Tipe Konten</label>
<select
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Tipe</option>
<option value="1">📸 Image</option>
<option value="2">🎬 Video</option>
<option value="3">📝 Text</option>
<option value="4">🎵 Audio</option>
</select>
</div>
{/* Date Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Rentang Tanggal
</label>
<div className="space-y-2">
<Input
type="date"
placeholder="Tanggal Mulai"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
handleFilterChange();
}}
/>
<Input
type="date"
placeholder="Tanggal Akhir"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
handleFilterChange();
}}
/>
</div>
</div>
{/* Clear Filters */}
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setSelectedCategory("all");
setSelectedType("all");
setStartDate("");
setEndDate("");
setCurrentPage(1);
}}
className="w-full"
>
Reset Filter
</Button>
</div>
</Card>
</div>
{/* Main Content */}
<div className="lg:w-3/4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Daftar Artikel</h1>
<p className="text-gray-600">
Menampilkan {totalData} artikel
{searchTerm && ` untuk "${searchTerm}"`}
{selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`}
</p>
</div>
{/* Articles Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-3 bg-gray-200 rounded w-full"></div>
</div>
</Card>
))}
</div>
) : articles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{articles.map((article) => (
<Card key={article.id} className="overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="w-full h-48 relative">
<Link href={getLink(article)}>
<Image
src={article.thumbnailUrl || "/placeholder.png"}
alt={article.title || "No Title"}
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/>
</Link>
</div>
<div className="p-4">
<div className="flex gap-2 mb-2">
<Badge color="primary" className="text-xs">
{article.clientName}
</Badge>
<Badge color="secondary" className="text-xs">
{article.categoryName || "Tanpa Kategori"}
</Badge>
</div>
<p className="text-xs text-gray-500 mb-2">
{formatTanggal(article.createdAt)}
</p>
<Link href={getLink(article)}>
<h3 className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{article.title}
</h3>
</Link>
{/* <p className="text-xs text-gray-600 mb-4 line-clamp-2">
{article.description || article.content || ""}
</p> */}
<div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer hover:text-blue-600" />
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
</div>
<Button
onClick={() => handleSave(article.id)}
disabled={bookmarkedIds.has(Number(article.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(article.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{bookmarkedIds.has(Number(article.id)) ? "Saved" : "Save"}
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card className="p-8 text-center">
<div className="text-gray-500">
<p className="text-lg mb-2">Tidak ada artikel ditemukan</p>
<p className="text-sm">Coba ubah filter atau kata kunci pencarian</p>
</div>
</Card>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNumber = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
if (pageNumber > totalPages) return null;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
onClick={() => handlePageChange(pageNumber)}
isActive={currentPage === pageNumber}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,501 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ThumbsUp, ThumbsDown, Search, Filter, Calendar, Tag } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { listArticles } from "@/service/landing/landing";
import { getArticleCategories } from "@/service/categories/article-categories";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// Format tanggal
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
new Date(dateString)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(/\./g, ":") + " WIB"
);
}
// Function to get link based on typeId
function getLink(item: any) {
switch (item?.typeId) {
case 1:
return `/content/image/detail/${item?.id}`;
case 2:
return `/content/video/detail/${item?.id}`;
case 3:
return `/content/text/detail/${item?.id}`;
case 4:
return `/content/audio/detail/${item?.id}`;
default:
return "#";
}
}
export default function ArticleListPage() {
const [articles, setArticles] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [selectedType, setSelectedType] = useState<string>("3");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalData, setTotalData] = useState(0);
const router = useRouter();
const searchParams = useSearchParams();
const MySwal = withReactContent(Swal);
const itemsPerPage = 6;
// Load categories on component mount
useEffect(() => {
loadCategories();
}, []);
// Load articles when filters change
useEffect(() => {
loadArticles();
}, [currentPage, selectedCategory, selectedType, searchTerm, startDate, endDate]);
// Sync bookmarks
useEffect(() => {
syncBookmarks();
}, []);
async function loadCategories() {
try {
const response = await getArticleCategories();
if (response?.data?.data) {
setCategories(response.data.data);
}
} catch (error) {
console.error("Error loading categories:", error);
}
}
async function loadArticles() {
try {
setLoading(true);
const categoryId = selectedCategory === "all" ? undefined : selectedCategory;
const typeId = selectedType === "all" ? undefined : parseInt(selectedType);
const response = await listArticles(
currentPage,
itemsPerPage,
typeId,
searchTerm || undefined,
categoryId,
"createdAt"
);
if (response?.data?.data) {
const articlesData = response.data.data;
setArticles(articlesData);
setTotalPages(response.data.meta.totalPage || 1);
setTotalData(response.data.meta.count || 0);
}
} catch (error) {
console.error("Error loading articles:", error);
} finally {
setLoading(false);
}
}
async function syncBookmarks() {
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
try {
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 (error) {
console.error("Error syncing bookmarks:", error);
}
}
}
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.",
});
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
loadArticles();
};
const handleFilterChange = () => {
setCurrentPage(1);
loadArticles();
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const getTypeLabel = (typeId: number) => {
switch (typeId) {
case 1: return "📸 Image";
case 2: return "🎬 Video";
case 3: return "📝 Text";
case 4: return "🎵 Audio";
default: return "📄 Content";
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-2 lg:px-4">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filter Sidebar */}
<div className="lg:w-1/4">
<Card className="p-6 sticky top-4">
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-5 h-5 text-blue-600" />
<h3 className="text-lg font-semibold">Filter Artikel</h3>
</div>
{/* Search */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Cari Artikel</label>
<form onSubmit={handleSearch} className="flex gap-2">
<Input
type="text"
placeholder="Masukkan kata kunci..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1"
/>
<Button type="submit" size="sm">
<Search className="w-4 h-4" />
</Button>
</form>
</div>
{/* Category Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Tag className="w-4 h-4" />
Kategori
</label>
<select
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Kategori</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.title}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Tipe Konten</label>
<select
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Tipe</option>
<option value="1">📸 Image</option>
<option value="2">🎬 Video</option>
<option value="3">📝 Text</option>
<option value="4">🎵 Audio</option>
</select>
</div>
{/* Date Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Rentang Tanggal
</label>
<div className="space-y-2">
<Input
type="date"
placeholder="Tanggal Mulai"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
handleFilterChange();
}}
/>
<Input
type="date"
placeholder="Tanggal Akhir"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
handleFilterChange();
}}
/>
</div>
</div>
{/* Clear Filters */}
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setSelectedCategory("all");
setSelectedType("all");
setStartDate("");
setEndDate("");
setCurrentPage(1);
}}
className="w-full"
>
Reset Filter
</Button>
</div>
</Card>
</div>
{/* Main Content */}
<div className="lg:w-3/4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Daftar Artikel</h1>
<p className="text-gray-600">
Menampilkan {totalData} artikel
{searchTerm && ` untuk "${searchTerm}"`}
{selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`}
</p>
</div>
{/* Articles Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-3 bg-gray-200 rounded w-full"></div>
</div>
</Card>
))}
</div>
) : articles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{articles.map((article) => (
<Card key={article.id} className="overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="w-full h-48 relative">
<Link href={getLink(article)}>
<Image
src={article.thumbnailUrl || "/placeholder.png"}
alt={article.title || "No Title"}
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/>
</Link>
</div>
<div className="p-4">
<div className="flex gap-2 mb-2">
<Badge color="primary" className="text-xs">
{article.clientName}
</Badge>
<Badge color="secondary" className="text-xs">
{article.categoryName || "Tanpa Kategori"}
</Badge>
</div>
<p className="text-xs text-gray-500 mb-2">
{formatTanggal(article.createdAt)}
</p>
<Link href={getLink(article)}>
<h3 className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{article.title}
</h3>
</Link>
{/* <p className="text-xs text-gray-600 mb-4 line-clamp-2">
{article.description || article.content || ""}
</p> */}
<div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer hover:text-blue-600" />
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
</div>
<Button
onClick={() => handleSave(article.id)}
disabled={bookmarkedIds.has(Number(article.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(article.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{bookmarkedIds.has(Number(article.id)) ? "Saved" : "Save"}
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card className="p-8 text-center">
<div className="text-gray-500">
<p className="text-lg mb-2">Tidak ada artikel ditemukan</p>
<p className="text-sm">Coba ubah filter atau kata kunci pencarian</p>
</div>
</Card>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNumber = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
if (pageNumber > totalPages) return null;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
onClick={() => handlePageChange(pageNumber)}
isActive={currentPage === pageNumber}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,501 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ThumbsUp, ThumbsDown, Search, Filter, Calendar, Tag } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { listArticles } from "@/service/landing/landing";
import { getArticleCategories } from "@/service/categories/article-categories";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// Format tanggal
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
new Date(dateString)
.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace(/\./g, ":") + " WIB"
);
}
// Function to get link based on typeId
function getLink(item: any) {
switch (item?.typeId) {
case 1:
return `/content/image/detail/${item?.id}`;
case 2:
return `/content/video/detail/${item?.id}`;
case 3:
return `/content/text/detail/${item?.id}`;
case 4:
return `/content/audio/detail/${item?.id}`;
default:
return "#";
}
}
export default function ArticleListPage() {
const [articles, setArticles] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [selectedType, setSelectedType] = useState<string>("2");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalData, setTotalData] = useState(0);
const router = useRouter();
const searchParams = useSearchParams();
const MySwal = withReactContent(Swal);
const itemsPerPage = 6;
// Load categories on component mount
useEffect(() => {
loadCategories();
}, []);
// Load articles when filters change
useEffect(() => {
loadArticles();
}, [currentPage, selectedCategory, selectedType, searchTerm, startDate, endDate]);
// Sync bookmarks
useEffect(() => {
syncBookmarks();
}, []);
async function loadCategories() {
try {
const response = await getArticleCategories();
if (response?.data?.data) {
setCategories(response.data.data);
}
} catch (error) {
console.error("Error loading categories:", error);
}
}
async function loadArticles() {
try {
setLoading(true);
const categoryId = selectedCategory === "all" ? undefined : selectedCategory;
const typeId = selectedType === "all" ? undefined : parseInt(selectedType);
const response = await listArticles(
currentPage,
itemsPerPage,
typeId,
searchTerm || undefined,
categoryId,
"createdAt"
);
if (response?.data?.data) {
const articlesData = response.data.data;
setArticles(articlesData);
setTotalPages(response.data.meta.totalPage || 1);
setTotalData(response.data.meta.count || 0);
}
} catch (error) {
console.error("Error loading articles:", error);
} finally {
setLoading(false);
}
}
async function syncBookmarks() {
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
try {
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 (error) {
console.error("Error syncing bookmarks:", error);
}
}
}
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.",
});
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
loadArticles();
};
const handleFilterChange = () => {
setCurrentPage(1);
loadArticles();
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const getTypeLabel = (typeId: number) => {
switch (typeId) {
case 1: return "📸 Image";
case 2: return "🎬 Video";
case 3: return "📝 Text";
case 4: return "🎵 Audio";
default: return "📄 Content";
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-2 lg:px-4">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filter Sidebar */}
<div className="lg:w-1/4">
<Card className="p-6 sticky top-4">
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<Filter className="w-5 h-5 text-blue-600" />
<h3 className="text-lg font-semibold">Filter Artikel</h3>
</div>
{/* Search */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Cari Artikel</label>
<form onSubmit={handleSearch} className="flex gap-2">
<Input
type="text"
placeholder="Masukkan kata kunci..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1"
/>
<Button type="submit" size="sm">
<Search className="w-4 h-4" />
</Button>
</form>
</div>
{/* Category Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Tag className="w-4 h-4" />
Kategori
</label>
<select
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Kategori</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.title}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Tipe Konten</label>
<select
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
handleFilterChange();
}}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Tipe</option>
<option value="1">📸 Image</option>
<option value="2">🎬 Video</option>
<option value="3">📝 Text</option>
<option value="4">🎵 Audio</option>
</select>
</div>
{/* Date Filter */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Rentang Tanggal
</label>
<div className="space-y-2">
<Input
type="date"
placeholder="Tanggal Mulai"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
handleFilterChange();
}}
/>
<Input
type="date"
placeholder="Tanggal Akhir"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
handleFilterChange();
}}
/>
</div>
</div>
{/* Clear Filters */}
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setSelectedCategory("all");
setSelectedType("all");
setStartDate("");
setEndDate("");
setCurrentPage(1);
}}
className="w-full"
>
Reset Filter
</Button>
</div>
</Card>
</div>
{/* Main Content */}
<div className="lg:w-3/4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Daftar Artikel</h1>
<p className="text-gray-600">
Menampilkan {totalData} artikel
{searchTerm && ` untuk "${searchTerm}"`}
{selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`}
</p>
</div>
{/* Articles Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-3 bg-gray-200 rounded w-full"></div>
</div>
</Card>
))}
</div>
) : articles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{articles.map((article) => (
<Card key={article.id} className="overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="w-full h-48 relative">
<Link href={getLink(article)}>
<Image
src={article.thumbnailUrl || "/placeholder.png"}
alt={article.title || "No Title"}
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/>
</Link>
</div>
<div className="p-4">
<div className="flex gap-2 mb-2">
<Badge color="primary" className="text-xs">
{article.clientName}
</Badge>
<Badge color="secondary" className="text-xs">
{article.categoryName || "Tanpa Kategori"}
</Badge>
</div>
<p className="text-xs text-gray-500 mb-2">
{formatTanggal(article.createdAt)}
</p>
<Link href={getLink(article)}>
<h3 className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{article.title}
</h3>
</Link>
{/* <p className="text-xs text-gray-600 mb-4 line-clamp-2">
{article.description || article.content || ""}
</p> */}
<div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer hover:text-blue-600" />
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
</div>
<Button
onClick={() => handleSave(article.id)}
disabled={bookmarkedIds.has(Number(article.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(article.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{bookmarkedIds.has(Number(article.id)) ? "Saved" : "Save"}
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card className="p-8 text-center">
<div className="text-gray-500">
<p className="text-lg mb-2">Tidak ada artikel ditemukan</p>
<p className="text-sm">Coba ubah filter atau kata kunci pencarian</p>
</div>
</Card>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNumber = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
if (pageNumber > totalPages) return null;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
onClick={() => handlePageChange(pageNumber)}
isActive={currentPage === pageNumber}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -37,7 +37,7 @@ function formatTanggal(dateString: string) {
export default function MediaUpdate() {
const [tab, setTab] = useState<"latest" | "popular">("latest");
const [contentType, setContentType] = useState<"all" | "audiovisual" | "audio" | "foto" | "text">("all");
const [contentType, setContentType] = useState<"audiovisual" | "audio" | "foto" | "text">("foto");
const [dataToRender, setDataToRender] = useState<any[]>([]);
const [filteredData, setFilteredData] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
@ -94,6 +94,23 @@ export default function MediaUpdate() {
}
}
// Function to get content type link for "Lihat lebih banyak" button
function getContentTypeLink() {
switch (contentType) {
case "audio":
return "/content/audio";
case "foto":
return "/content/image";
case "audiovisual":
return "/content/video";
case "text":
return "/content/text";
case "all":
default:
return "/content/image"; // Default to image page
}
}
// Function to filter data by content type
function filterDataByContentType() {
if (contentType === "all") {
@ -312,7 +329,7 @@ export default function MediaUpdate() {
<div className="flex justify-center mb-8">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg">
<div className="flex flex-wrap justify-center gap-2">
<button
{/* <button
onClick={() => setContentType("all")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "all"
@ -321,6 +338,17 @@ export default function MediaUpdate() {
}`}
>
📋 Semua
</button> */}
<button
onClick={() => setContentType("foto")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "foto"
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
}`}
>
📸 Foto
</button>
<button
onClick={() => setContentType("audiovisual")}
@ -342,16 +370,6 @@ export default function MediaUpdate() {
>
🎵 Audio
</button>
<button
onClick={() => setContentType("foto")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "foto"
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
}`}
>
📸 Foto
</button>
<button
onClick={() => setContentType("text")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
@ -437,23 +455,21 @@ export default function MediaUpdate() {
</Swiper>
)}
{/* Lihat lebih banyak */}
<div className="text-center mt-10">
<Link
href={
tab === "latest"
? "https://mediahub.polri.go.id/"
: "https://tribratanews.polri.go.id/"
}
>
<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 - hanya muncul jika ada data */}
{filteredData.length > 0 && (
<div className="text-center mt-10">
<Link
href={getContentTypeLink()}
>
Lihat Lebih Banyak
</Button>
</Link>
</div>
<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>
</Link>
</div>
)}
</div>
</section>
);

View File

@ -170,13 +170,13 @@ export async function listData(
// New Articles API for public/landing usage
export async function listArticles(
page = 1,
totalPage = 10,
limit = 10,
typeId?: number,
search?: string,
categoryId?: string,
sortBy = "createdAt"
) {
let url = `articles?page=${page}&totalPage=${totalPage}&isPublish=true`;
let url = `articles?page=${page}&limit=${limit}&isPublish=true`;
if (typeId !== undefined) url += `&typeId=${typeId}`;
if (search) url += `&title=${encodeURIComponent(search)}`;