feat: update list article per tenant, header per tenant, etc
This commit is contained in:
parent
760aa93b2d
commit
e6f9d50786
|
|
@ -0,0 +1,11 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import AudioDetail from "@/components/main/content/audio-detail";
|
||||||
|
|
||||||
|
export default function DetailAudioInfo() {
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
return <AudioDetail id={id} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,504 @@
|
||||||
|
"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 { useParams, 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 params = useParams();
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
|
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",
|
||||||
|
slug
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import ImageDetail from "@/components/main/content/image-detail";
|
||||||
|
|
||||||
|
export default function DetailImageInfo() {
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
return <ImageDetail id={id} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,504 @@
|
||||||
|
"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 { useParams, 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 params = useParams();
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
|
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",
|
||||||
|
slug
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import DocumentDetail from "@/components/main/content/document-detail";
|
||||||
|
|
||||||
|
export default function DetailDocumentInfo() {
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
return <DocumentDetail id={id} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,504 @@
|
||||||
|
"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 { useParams, 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 params = useParams();
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
|
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",
|
||||||
|
slug
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import DetailCommentVideo from "@/components/main/comment-detail-video";
|
||||||
|
|
||||||
|
export default async function DetailCommentInfo() {
|
||||||
|
return <DetailCommentVideo />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import DetailVideo from "@/components/main/content/video-detail";
|
||||||
|
|
||||||
|
export default function DetailVideoInfo() {
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id as string;
|
||||||
|
|
||||||
|
return <DetailVideo id={id} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,504 @@
|
||||||
|
"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 { useParams, 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 params = useParams();
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
|
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",
|
||||||
|
slug
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Footer from "@/components/landing-page/footer";
|
||||||
|
import Navbar from "@/components/landing-page/navbar";
|
||||||
|
|
||||||
|
const layout = async ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layout;
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import LayoutProvider from "@/providers/layout.provider";
|
|
||||||
import LayoutContentProvider from "@/providers/content.provider";
|
|
||||||
import DashCodeSidebar from "@/components/partials/sidebar";
|
|
||||||
import DashCodeFooter from "@/components/partials/footer";
|
|
||||||
import ThemeCustomize from "@/components/partials/customizer";
|
|
||||||
import DashCodeHeader from "@/components/partials/header";
|
|
||||||
|
|
||||||
import { redirect } from "@/components/navigation";
|
|
||||||
import Footer from "@/components/landing-page/footer";
|
|
||||||
import Navbar from "@/components/landing-page/navbar";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
|
|
||||||
const layout = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const params = useParams();
|
|
||||||
const poldaName: any = params?.polda_name;
|
|
||||||
return (
|
|
||||||
// children
|
|
||||||
// ) : (
|
|
||||||
<>
|
|
||||||
<Navbar />
|
|
||||||
{children}
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default layout;
|
|
||||||
|
|
@ -2,23 +2,77 @@
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { getClientBySlug, PublicClient } from "@/service/client/public-clients";
|
||||||
|
|
||||||
export const DynamicLogoTenant = () => {
|
export const DynamicLogoTenant = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const tenant = pathname?.split("/")[3];
|
const tenant = pathname?.split("/")[3];
|
||||||
|
const [clientData, setClientData] = useState<PublicClient | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchClientData = async () => {
|
||||||
|
if (!tenant || !pathname?.includes("/tenant")) {
|
||||||
|
setClientData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await getClientBySlug(tenant);
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
setError("Client not found");
|
||||||
|
} else {
|
||||||
|
setClientData(response.data?.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching client data:", err);
|
||||||
|
setError("Failed to load client data");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchClientData();
|
||||||
|
}, [tenant, pathname]);
|
||||||
|
|
||||||
|
if (!pathname?.includes("/tenant") || !tenant) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="h-[80px] w-[80px] ml-10 bg-gray-200 animate-pulse rounded"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !clientData?.logoUrl) {
|
||||||
|
return (
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="h-[80px] w-[80px] ml-6 bg-gray-100 rounded flex items-center justify-center">
|
||||||
|
<span className="text-xs text-gray-500">No Logo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{pathname?.includes("/tenant") && (
|
<Image
|
||||||
<Image
|
priority={true}
|
||||||
priority={true}
|
src={clientData.logoUrl}
|
||||||
src={`/logo/${tenant}.png`}
|
alt={`${clientData.name} Logo`}
|
||||||
alt="Logo"
|
width={60}
|
||||||
width={1920}
|
height={60}
|
||||||
height={1080}
|
className="object-contain h-[80px] w-[80px] ml-6"
|
||||||
className="object-contain h-[50px] w-[50px] ml-10"
|
onError={() => setError("Failed to load image")}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import withReactContent from "sweetalert2-react-content";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
import { listData, listArticles } from "@/service/landing/landing";
|
import { listData, listArticles } from "@/service/landing/landing";
|
||||||
|
|
@ -14,6 +14,12 @@ import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
// Get slug from URL params
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -25,7 +31,8 @@ export default function Header() {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
"createdAt"
|
"createdAt",
|
||||||
|
slug
|
||||||
);
|
);
|
||||||
|
|
||||||
let articlesData: any[] = [];
|
let articlesData: any[] = [];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||||
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import { listData, listArticles } from "@/service/landing/landing";
|
import { listData, listArticles } from "@/service/landing/landing";
|
||||||
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
|
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
|
||||||
import { getCookiesDecrypt } from "@/lib/utils";
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
|
@ -44,18 +44,23 @@ export default function MediaUpdate() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentTypeId, setCurrentTypeId] = useState<string>("1");
|
const [currentTypeId, setCurrentTypeId] = useState<string>("1");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
// Get slug from URL params
|
||||||
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(tab);
|
fetchData(tab);
|
||||||
}, [tab]);
|
}, [tab, slug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentType !== "all") {
|
// if (contentType !== "all") {
|
||||||
fetchData(tab);
|
// fetchData(tab);
|
||||||
} else {
|
// } else {
|
||||||
filterDataByContentType();
|
// filterDataByContentType();
|
||||||
}
|
// }
|
||||||
|
fetchData(tab);
|
||||||
}, [contentType]);
|
}, [contentType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -98,25 +103,24 @@ export default function MediaUpdate() {
|
||||||
function getContentTypeLink() {
|
function getContentTypeLink() {
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case "audio":
|
case "audio":
|
||||||
return "/content/audio";
|
return "/tenant/" + slug + "/content/audio";
|
||||||
case "foto":
|
case "foto":
|
||||||
return "/content/image";
|
return "/tenant/" + slug + "/content/image";
|
||||||
case "audiovisual":
|
case "audiovisual":
|
||||||
return "/content/video";
|
return "/tenant/" + slug + "/content/video";
|
||||||
case "text":
|
case "text":
|
||||||
return "/content/text";
|
return "/tenant/" + slug + "/content/text";
|
||||||
case "all":
|
|
||||||
default:
|
default:
|
||||||
return "/content/image"; // Default to image page
|
return "/tenant/" + slug + "/content/image"; // Default to image page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to filter data by content type
|
// Function to filter data by content type
|
||||||
function filterDataByContentType() {
|
function filterDataByContentType() {
|
||||||
if (contentType === "all") {
|
// if (contentType === "all") {
|
||||||
setFilteredData(dataToRender);
|
// setFilteredData(dataToRender);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const filtered = dataToRender.filter((item) => {
|
const filtered = dataToRender.filter((item) => {
|
||||||
// Determine content type based on item properties
|
// Determine content type based on item properties
|
||||||
|
|
@ -147,17 +151,18 @@ export default function MediaUpdate() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Determine typeId based on contentType
|
// Determine typeId based on contentType
|
||||||
const typeId = contentType === "all" ? 1 : parseInt(getTypeIdByContentType(contentType));
|
const typeId = parseInt(getTypeIdByContentType(contentType));
|
||||||
setCurrentTypeId(typeId.toString());
|
setCurrentTypeId(typeId.toString());
|
||||||
|
|
||||||
// 🔹 Ambil data artikel
|
// 🔹 Ambil data artikel
|
||||||
const response = await listArticles(
|
const response = await listArticles(
|
||||||
1,
|
1,
|
||||||
20,
|
10,
|
||||||
typeId, // Dynamic typeId based on content type
|
typeId, // Dynamic typeId based on content type
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
section === "latest" ? "createdAt" : "viewCount"
|
section === "latest" ? "createdAt" : "viewCount",
|
||||||
|
slug
|
||||||
);
|
);
|
||||||
|
|
||||||
let articlesData: any[] = [];
|
let articlesData: any[] = [];
|
||||||
|
|
@ -181,7 +186,7 @@ export default function MediaUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Normalisasi struktur data
|
// 🔹 Normalisasi struktur data
|
||||||
const transformedData = articlesData.map((article: any) => ({
|
let transformedData = articlesData.map((article: any) => ({
|
||||||
id: article.id,
|
id: article.id,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
category:
|
category:
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,9 @@ export async function getPublicClients() {
|
||||||
const url = "/clients/public";
|
const url = "/clients/public";
|
||||||
return httpGetInterceptor(url);
|
return httpGetInterceptor(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service function to get client by slug
|
||||||
|
export async function getClientBySlug(slug: string) {
|
||||||
|
const url = `/clients/public/slug/${slug}`;
|
||||||
|
return httpGetInterceptor(url);
|
||||||
|
}
|
||||||
|
|
@ -174,13 +174,15 @@ export async function listArticles(
|
||||||
typeId?: number,
|
typeId?: number,
|
||||||
search?: string,
|
search?: string,
|
||||||
categoryId?: string,
|
categoryId?: string,
|
||||||
sortBy = "createdAt"
|
sortBy = "createdAt",
|
||||||
|
clientSlug?: string
|
||||||
) {
|
) {
|
||||||
let url = `articles?page=${page}&limit=${limit}&isPublish=true`;
|
let url = `articles?page=${page}&limit=${limit}&isPublish=true`;
|
||||||
|
|
||||||
if (typeId !== undefined) url += `&typeId=${typeId}`;
|
if (typeId !== undefined) url += `&typeId=${typeId}`;
|
||||||
if (search) url += `&title=${encodeURIComponent(search)}`;
|
if (search) url += `&title=${encodeURIComponent(search)}`;
|
||||||
if (categoryId) url += `&categoryId=${categoryId}`;
|
if (categoryId) url += `&categoryId=${categoryId}`;
|
||||||
|
if (clientSlug) url += `&clientSlug=${clientSlug}`;
|
||||||
// if (sortBy) url += `&sortBy=${sortBy}`;
|
// if (sortBy) url += `&sortBy=${sortBy}`;
|
||||||
|
|
||||||
return await httpGetInterceptor(url);
|
return await httpGetInterceptor(url);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue