pull main
This commit is contained in:
commit
8d4d6f4768
|
|
@ -26,6 +26,7 @@ import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||||
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
||||||
import { useWorkflowStatusCheck } from "@/hooks/useWorkflowStatusCheck";
|
import { useWorkflowStatusCheck } from "@/hooks/useWorkflowStatusCheck";
|
||||||
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
||||||
import {
|
import {
|
||||||
CreateApprovalWorkflowWithClientSettingsRequest,
|
CreateApprovalWorkflowWithClientSettingsRequest,
|
||||||
UserLevelsCreateRequest,
|
UserLevelsCreateRequest,
|
||||||
|
|
@ -60,7 +61,7 @@ import { close, loading } from "@/config/swal";
|
||||||
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
||||||
|
|
||||||
function TenantSettingsContentTable() {
|
function TenantSettingsContentTable() {
|
||||||
const [activeTab, setActiveTab] = useState("workflows");
|
const [activeTab, setActiveTab] = useLocalStorage('tenant-settings-active-tab', 'profile');
|
||||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||||
const [workflow, setWorkflow] =
|
const [workflow, setWorkflow] =
|
||||||
useState<ComprehensiveWorkflowResponse | null>(null);
|
useState<ComprehensiveWorkflowResponse | null>(null);
|
||||||
|
|
@ -220,7 +221,7 @@ function TenantSettingsContentTable() {
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="tenant"
|
value="profile"
|
||||||
className="flex items-center gap-2 border rounded-lg"
|
className="flex items-center gap-2 border rounded-lg"
|
||||||
>
|
>
|
||||||
<WorkflowIcon className="h-4 w-4" />
|
<WorkflowIcon className="h-4 w-4" />
|
||||||
|
|
@ -243,7 +244,7 @@ function TenantSettingsContentTable() {
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Approval Workflows Tab */}
|
{/* Approval Workflows Tab */}
|
||||||
<TabsContent value="tenant" className="space-y-6 border rounded-lg">
|
<TabsContent value="profile" className="space-y-6 border rounded-lg">
|
||||||
<DetailTenant id={10} />
|
<DetailTenant id={10} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="workflows" className="space-y-6 border rounded-lg">
|
<TabsContent value="workflows" className="space-y-6 border rounded-lg">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -687,6 +687,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
disabled={isSubmitting || isLoading || isLoadingData}
|
disabled={isSubmitting || isLoading || isLoadingData}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import withReactContent from "sweetalert2-react-content";
|
||||||
import { errorAutoClose, loading, successAutoClose } from "@/lib/swal";
|
import { errorAutoClose, loading, successAutoClose } from "@/lib/swal";
|
||||||
import { close } from "@/config/swal";
|
import { close } from "@/config/swal";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { getClientProfile, updateClientProfile, uploadClientLogo, ClientProfile } from "@/service/client/client-profile";
|
||||||
|
import { SaveIcon } from "@/components/icons";
|
||||||
|
|
||||||
// ✅ Zod Schema Validasi
|
// ✅ Zod Schema Validasi
|
||||||
const companySchema = z.object({
|
const companySchema = z.object({
|
||||||
|
|
@ -58,6 +60,8 @@ export default function TenantCompanyUpdateForm({
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
const [previewLogo, setPreviewLogo] = useState<string | null>(null);
|
const [previewLogo, setPreviewLogo] = useState<string | null>(null);
|
||||||
const [loadingData, setLoadingData] = useState(false);
|
const [loadingData, setLoadingData] = useState(false);
|
||||||
|
const [clientProfile, setClientProfile] = useState<ClientProfile | null>(null);
|
||||||
|
const [selectedLogoFile, setSelectedLogoFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
|
@ -82,26 +86,30 @@ export default function TenantCompanyUpdateForm({
|
||||||
async function fetchCompanyData() {
|
async function fetchCompanyData() {
|
||||||
setLoadingData(true);
|
setLoadingData(true);
|
||||||
try {
|
try {
|
||||||
// TODO: ganti dengan service API kamu (misalnya getTenantCompanyDetail)
|
const response = await getClientProfile();
|
||||||
const response = initialData; // simulasi
|
if (response?.data?.success && response.data.data) {
|
||||||
if (response) {
|
const data = response.data.data;
|
||||||
setValue("companyName", response.companyName || "");
|
setClientProfile(data);
|
||||||
setValue("address", response.address || "");
|
|
||||||
setValue("phone", response.phone || "");
|
// Set form values
|
||||||
setValue("website", response.website || "");
|
setValue("companyName", data.name || "");
|
||||||
setValue("description", response.description || "");
|
setValue("address", data.address || "");
|
||||||
setValue("isActive", response.isActive ?? true);
|
setValue("phone", data.phoneNumber || "");
|
||||||
setPreviewLogo(response.logoUrl || null);
|
setValue("website", data.website || "");
|
||||||
|
setValue("description", data.description || "");
|
||||||
|
setValue("isActive", data.isActive ?? true);
|
||||||
|
setPreviewLogo(data.logoUrl || null);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error("❌ Gagal memuat data perusahaan:", err);
|
console.error("❌ Gagal memuat data perusahaan:", err);
|
||||||
|
errorAutoClose(err?.message || "Gagal memuat data perusahaan");
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingData(false);
|
setLoadingData(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCompanyData();
|
fetchCompanyData();
|
||||||
}, [initialData, setValue]);
|
}, [setValue]);
|
||||||
|
|
||||||
// ✅ Fungsi Upload Logo
|
// ✅ Fungsi Upload Logo
|
||||||
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -117,40 +125,78 @@ export default function TenantCompanyUpdateForm({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validasi ukuran file (max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
MySwal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Ukuran file terlalu besar",
|
||||||
|
text: "Ukuran file maksimal 5MB",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setValue("logo", file);
|
setValue("logo", file);
|
||||||
|
setSelectedLogoFile(file);
|
||||||
setPreviewLogo(URL.createObjectURL(file));
|
setPreviewLogo(URL.createObjectURL(file));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Fungsi Remove Logo
|
||||||
|
const handleRemoveLogo = () => {
|
||||||
|
setValue("logo", undefined);
|
||||||
|
setSelectedLogoFile(null);
|
||||||
|
setPreviewLogo(clientProfile?.logoUrl || null);
|
||||||
|
};
|
||||||
|
|
||||||
// ✅ Submit Form
|
// ✅ Submit Form
|
||||||
const onSubmit = async (data: CompanySchema) => {
|
const onSubmit = async (data: CompanySchema) => {
|
||||||
try {
|
try {
|
||||||
loading();
|
loading();
|
||||||
|
|
||||||
const formData = new FormData();
|
// Update profile data terlebih dahulu
|
||||||
formData.append("companyName", data.companyName);
|
const updateData = {
|
||||||
formData.append("address", data.address);
|
name: data.companyName,
|
||||||
formData.append("phone", data.phone);
|
address: data.address,
|
||||||
formData.append("website", data.website || "");
|
phoneNumber: data.phone,
|
||||||
formData.append("description", data.description || "");
|
website: data.website || undefined,
|
||||||
formData.append("isActive", data.isActive ? "true" : "false");
|
description: data.description || undefined,
|
||||||
if (data.logo) formData.append("logo", data.logo);
|
};
|
||||||
|
|
||||||
console.log("📦 Payload:", Object.fromEntries(formData.entries()));
|
console.log("📦 Payload:", updateData);
|
||||||
|
|
||||||
// TODO: Ganti dengan service API kamu → misalnya updateTenantCompany(formData)
|
const response = await updateClientProfile(updateData);
|
||||||
// const response = await updateTenantCompany(id, formData);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // simulate
|
if (response?.data?.success) {
|
||||||
close();
|
// Jika ada logo yang dipilih, upload logo
|
||||||
|
if (selectedLogoFile) {
|
||||||
|
try {
|
||||||
|
console.log("📤 Uploading logo...");
|
||||||
|
const logoResponse = await uploadClientLogo(selectedLogoFile);
|
||||||
|
|
||||||
|
if (logoResponse?.data?.success) {
|
||||||
|
console.log("✅ Logo uploaded successfully");
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Logo upload failed, but profile updated");
|
||||||
|
}
|
||||||
|
} catch (logoError: any) {
|
||||||
|
console.error("❌ Error uploading logo:", logoError);
|
||||||
|
// Tidak menghentikan proses jika logo gagal diupload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
successAutoClose("Data perusahaan berhasil diperbarui.");
|
close();
|
||||||
|
successAutoClose("Data perusahaan berhasil diperbarui.");
|
||||||
setTimeout(() => {
|
|
||||||
if (onSuccess) onSuccess();
|
setTimeout(() => {
|
||||||
}, 2000);
|
if (onSuccess) onSuccess();
|
||||||
} catch (err) {
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
errorAutoClose("Gagal memperbarui data perusahaan.");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
close();
|
close();
|
||||||
console.error("❌ Gagal update perusahaan:", err);
|
console.error("❌ Gagal update perusahaan:", err);
|
||||||
errorAutoClose("Terjadi kesalahan saat memperbarui data perusahaan.");
|
errorAutoClose(err?.message || "Terjadi kesalahan saat memperbarui data perusahaan.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -188,16 +234,32 @@ export default function TenantCompanyUpdateForm({
|
||||||
className="max-w-xs"
|
className="max-w-xs"
|
||||||
/>
|
/>
|
||||||
{previewLogo && (
|
{previewLogo && (
|
||||||
<div className="relative w-20 h-20 rounded-lg overflow-hidden border">
|
<div className="flex items-center gap-2">
|
||||||
<Image
|
<div className="relative w-20 h-20 rounded-lg overflow-hidden border">
|
||||||
src={previewLogo}
|
<Image
|
||||||
alt="Company Logo"
|
src={previewLogo}
|
||||||
fill
|
alt="Company Logo"
|
||||||
className="object-cover"
|
fill
|
||||||
/>
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedLogoFile && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRemoveLogo}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Format yang didukung: JPG, PNG, WebP. Maksimal 5MB.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nama Perusahaan */}
|
{/* Nama Perusahaan */}
|
||||||
|
|
@ -278,7 +340,7 @@ export default function TenantCompanyUpdateForm({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Aktif */}
|
{/* Status Aktif */}
|
||||||
<div className="flex items-center space-x-2">
|
{/* <div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="isActive"
|
name="isActive"
|
||||||
|
|
@ -290,14 +352,21 @@ export default function TenantCompanyUpdateForm({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Label>Aktif</Label>
|
<Label>Aktif</Label>
|
||||||
</div>
|
</div> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="flex justify-end gap-3">
|
<CardFooter className="flex justify-end gap-3">
|
||||||
<Button type="submit">Update</Button>
|
|
||||||
<Button type="button" variant="outline" onClick={() => onCancel?.()}>
|
<Button type="button" variant="outline" onClick={() => onCancel?.()}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<SaveIcon className="h-4 w-4" />
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,38 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { getArticleCategories, ArticleCategory } from "@/service/categories/article-categories";
|
||||||
|
|
||||||
export default function Category() {
|
export default function Category() {
|
||||||
const categories = [
|
const [categories, setCategories] = useState<ArticleCategory[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Fetch article categories
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCategories() {
|
||||||
|
try {
|
||||||
|
const response = await getArticleCategories();
|
||||||
|
if (response?.data?.success && response.data.data) {
|
||||||
|
// Filter hanya kategori yang aktif dan published
|
||||||
|
const activeCategories = response.data.data.filter(
|
||||||
|
(category: ArticleCategory) => category.isActive && category.isPublish
|
||||||
|
);
|
||||||
|
setCategories(activeCategories);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching article categories:", error);
|
||||||
|
// Fallback to static categories if API fails
|
||||||
|
setCategories([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCategories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fallback categories jika API gagal atau tidak ada data
|
||||||
|
const fallbackCategories = [
|
||||||
"PON XXI",
|
"PON XXI",
|
||||||
"OPERASI KETUPAT 2025",
|
"OPERASI KETUPAT 2025",
|
||||||
"HUT HUMAS KE-74",
|
"HUT HUMAS KE-74",
|
||||||
|
|
@ -14,22 +45,50 @@ export default function Category() {
|
||||||
"SEPUTAR PRESTASI",
|
"SEPUTAR PRESTASI",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const displayCategories = categories.length > 0 ? categories : fallbackCategories;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 py-10">
|
<section className="px-4 py-10">
|
||||||
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
|
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
|
||||||
<h2 className="text-xl font-semibold mb-5">
|
<h2 className="text-xl font-semibold mb-5">
|
||||||
10 Kategori Paling Populer
|
{loading ? "Memuat Kategori..." : `${displayCategories.length} Kategori Paling Populer`}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{categories.map((category, index) => (
|
{loading ? (
|
||||||
<button
|
// Loading skeleton
|
||||||
key={index}
|
<div className="flex flex-wrap gap-3">
|
||||||
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 transition"
|
{Array.from({ length: 10 }).map((_, index) => (
|
||||||
>
|
<div
|
||||||
{category}
|
key={index}
|
||||||
</button>
|
className="px-4 py-2 rounded border border-gray-200 bg-gray-100 animate-pulse"
|
||||||
))}
|
>
|
||||||
</div>
|
<div className="h-4 w-20 bg-gray-300 rounded"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{displayCategories.map((category, index) => {
|
||||||
|
// Handle both API data and fallback data
|
||||||
|
const categoryTitle = typeof category === 'string' ? category : category.title;
|
||||||
|
const categorySlug = typeof category === 'string' ? category.toLowerCase().replace(/\s+/g, '-') : category.slug;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
|
||||||
|
onClick={() => {
|
||||||
|
// Navigate to category page or search by category
|
||||||
|
console.log(`Category clicked: ${categoryTitle} (${categorySlug})`);
|
||||||
|
// TODO: Implement navigation to category page
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{categoryTitle}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,52 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Instagram, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Instagram } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { getPublicClients, PublicClient } from "@/service/client/public-clients";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { Navigation, Autoplay } from "swiper/modules";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
|
||||||
|
// Custom styles for Swiper
|
||||||
|
const swiperStyles = `
|
||||||
|
.client-swiper .swiper-button-next,
|
||||||
|
.client-swiper .swiper-button-prev {
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-swiper .swiper-button-next:after,
|
||||||
|
.client-swiper .swiper-button-prev:after {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-swiper .swiper-button-disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-swiper.swiper-centered .swiper-button-next,
|
||||||
|
.client-swiper.swiper-centered .swiper-button-prev {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-swiper.swiper-centered .swiper-wrapper {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.client-swiper .swiper-button-next,
|
||||||
|
.client-swiper .swiper-button-prev {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// const logos = [
|
// const logos = [
|
||||||
// { src: "/mabes.png", href: "/in/public/publication/kl" },
|
// { src: "/mabes.png", href: "/in/public/publication/kl" },
|
||||||
|
|
@ -27,63 +71,120 @@ const logos = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const [clients, setClients] = useState<PublicClient[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const scroll = (direction: "left" | "right") => {
|
// Fetch public clients
|
||||||
if (scrollRef.current) {
|
useEffect(() => {
|
||||||
scrollRef.current.scrollBy({
|
async function fetchClients() {
|
||||||
left: direction === "left" ? -200 : 200,
|
try {
|
||||||
behavior: "smooth",
|
const response = await getPublicClients();
|
||||||
});
|
if (response?.data?.success && response.data.data) {
|
||||||
|
setClients(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching public clients:", error);
|
||||||
|
// Fallback to static logos if API fails
|
||||||
|
setClients([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
fetchClients();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t bg-white text-center">
|
<footer className="border-t bg-white text-center">
|
||||||
|
<style jsx>{swiperStyles}</style>
|
||||||
<div className="max-w-[1350px] mx-auto">
|
<div className="max-w-[1350px] mx-auto">
|
||||||
<div className="py-6">
|
<div className="py-6">
|
||||||
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
||||||
Publikasi
|
Publikasi
|
||||||
</h2>
|
</h2>
|
||||||
<div className="relative flex items-center justify-center">
|
<div className="px-4 md:px-12">
|
||||||
{/* Left Scroll Button */}
|
<Swiper
|
||||||
<button
|
modules={[Navigation, Autoplay]}
|
||||||
onClick={() => scroll("left")}
|
spaceBetween={24}
|
||||||
className="absolute left-2 z-10 bg-white p-1 md:p-2 shadow rounded-full hidden md:inline-flex"
|
slidesPerView="auto"
|
||||||
|
centeredSlides={clients.length <= 4}
|
||||||
|
navigation={{
|
||||||
|
nextEl: ".swiper-button-next",
|
||||||
|
prevEl: ".swiper-button-prev",
|
||||||
|
}}
|
||||||
|
autoplay={{
|
||||||
|
delay: 3000,
|
||||||
|
disableOnInteraction: false,
|
||||||
|
}}
|
||||||
|
loop={clients.length > 4}
|
||||||
|
className={`client-swiper ${clients.length <= 4 ? 'swiper-centered' : ''}`}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4 md:w-5 md:h-5" />
|
{loading ? (
|
||||||
</button>
|
// Loading skeleton
|
||||||
|
Array.from({ length: 8 }).map((_, idx) => (
|
||||||
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
|
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
|
||||||
|
</SwiperSlide>
|
||||||
|
))
|
||||||
|
) : clients.length > 0 ? (
|
||||||
|
// Dynamic clients from API
|
||||||
|
clients.map((client, idx) => (
|
||||||
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
|
<a
|
||||||
|
href={`/in/tenant/${client.slug}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group block"
|
||||||
|
>
|
||||||
|
{client.logoUrl ? (
|
||||||
|
<Image
|
||||||
|
src={client.logoUrl}
|
||||||
|
alt={client.name}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// Fallback when no logo - menggunakan placeholder image
|
||||||
|
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
|
||||||
|
<Image
|
||||||
|
src="/logo-netidhub.png"
|
||||||
|
alt={`${client.name} placeholder`}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</SwiperSlide>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// Fallback to static logos if API fails or no data
|
||||||
|
logos.map((logo, idx) => (
|
||||||
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
|
<a
|
||||||
|
href={`/in/tenant/${logo.slug}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={logo.src}
|
||||||
|
alt={`logo-${idx}`}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</SwiperSlide>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Swiper>
|
||||||
|
|
||||||
{/* Scrollable Container */}
|
{/* Navigation Buttons */}
|
||||||
<div
|
<div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div>
|
||||||
ref={scrollRef}
|
<div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div>
|
||||||
className="flex gap-6 md:gap-14 overflow-x-auto no-scrollbar px-4 md:px-12"
|
|
||||||
>
|
|
||||||
{logos.map((logo, idx) => (
|
|
||||||
<a
|
|
||||||
key={idx}
|
|
||||||
href={`/in/tenant/${logo.slug}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={logo.src}
|
|
||||||
alt={`logo-${idx}`}
|
|
||||||
width={60}
|
|
||||||
height={60}
|
|
||||||
className="md:w-[80px] md:h-[80px] flex-shrink-0 object-contain hover:opacity-80 transition"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Scroll Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => scroll("right")}
|
|
||||||
className="absolute right-2 z-10 bg-white p-1 md:p-2 shadow rounded-full hidden md:inline-flex"
|
|
||||||
>
|
|
||||||
<ChevronRight className="w-4 h-4 md:w-5 md:h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export default function Header() {
|
||||||
createdAt: article.createdAt,
|
createdAt: article.createdAt,
|
||||||
smallThumbnailLink: article.thumbnailUrl,
|
smallThumbnailLink: article.thumbnailUrl,
|
||||||
fileTypeId: article.typeId,
|
fileTypeId: article.typeId,
|
||||||
tenantName: article.tenantName,
|
clientName: article.clientName,
|
||||||
categories: article.categories,
|
categories: article.categories,
|
||||||
label:
|
label:
|
||||||
article.typeId === 1
|
article.typeId === 1
|
||||||
|
|
@ -284,7 +284,7 @@ function Card({
|
||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
|
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
|
||||||
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
|
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
|
||||||
{item.tenantName}
|
{item.clientName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-orange-600">
|
<span className="text-orange-600">
|
||||||
{item.categories?.map((cat: any) => cat.title).join(", ")}
|
{item.categories?.map((cat: any) => cat.title).join(", ")}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Card } from "../ui/card";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
|
|
@ -36,80 +35,170 @@ function formatTanggal(dateString: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MediaUpdate() {
|
export default function MediaUpdate() {
|
||||||
const [tipeKonten, setTipeKonten] = useState<
|
const [tab, setTab] = useState<"latest" | "popular">("latest");
|
||||||
"image" | "video" | "text" | "audio"
|
const [contentType, setContentType] = useState<
|
||||||
>("image");
|
"audiovisual" | "audio" | "foto" | "text" | "all"
|
||||||
const [urutan, setUrutan] = useState<"latest" | "popular">("latest");
|
>("foto");
|
||||||
const [dataKonten, setDataKonten] = useState<any[]>([]);
|
const [dataToRender, setDataToRender] = useState<any[]>([]);
|
||||||
|
const [filteredData, setFilteredData] = useState<any[]>([]);
|
||||||
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [currentTypeId, setCurrentTypeId] = useState<string>("1");
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
// 🔹 Pemetaan tipe konten ke typeId API
|
// 🔹 Pemetaan tipe konten ke typeId API
|
||||||
const typeMap: Record<typeof tipeKonten, number> = {
|
// const typeMap: Record<typeof tipeKonten, number> = {
|
||||||
image: 1,
|
// image: 1,
|
||||||
video: 2,
|
// video: 2,
|
||||||
text: 3,
|
// text: 3,
|
||||||
audio: 4,
|
// audio: 4,
|
||||||
};
|
// };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ambilData();
|
if (contentType !== "all") {
|
||||||
}, [tipeKonten, urutan]);
|
fetchData(tab);
|
||||||
|
} else {
|
||||||
|
filterDataByContentType();
|
||||||
|
}
|
||||||
|
}, [contentType]);
|
||||||
|
|
||||||
async function ambilData() {
|
useEffect(() => {
|
||||||
|
filterDataByContentType();
|
||||||
|
}, [dataToRender]);
|
||||||
|
|
||||||
|
// Function to get typeId based on content type
|
||||||
|
function getTypeIdByContentType(contentType: string): string {
|
||||||
|
switch (contentType) {
|
||||||
|
case "audiovisual":
|
||||||
|
return "2"; // Video
|
||||||
|
case "foto":
|
||||||
|
return "1"; // Image
|
||||||
|
case "audio":
|
||||||
|
return "4"; // Audio
|
||||||
|
case "text":
|
||||||
|
return "3"; // Text
|
||||||
|
default:
|
||||||
|
return "1"; // Default to Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get link based on typeId (same as header.tsx)
|
||||||
|
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 "#";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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") {
|
||||||
|
setFilteredData(dataToRender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = dataToRender.filter((item) => {
|
||||||
|
// Determine content type based on item properties
|
||||||
|
const hasVideo = item.videoUrl || item.videoPath;
|
||||||
|
const hasAudio = item.audioUrl || item.audioPath;
|
||||||
|
const hasImage = item.smallThumbnailLink || item.thumbnailUrl || item.imageUrl;
|
||||||
|
const hasText = item.content || item.description;
|
||||||
|
|
||||||
|
switch (contentType) {
|
||||||
|
case "audiovisual":
|
||||||
|
return hasVideo && (hasAudio || hasImage);
|
||||||
|
case "audio":
|
||||||
|
return hasAudio && !hasVideo;
|
||||||
|
case "foto":
|
||||||
|
return hasImage && !hasVideo && !hasAudio;
|
||||||
|
case "text":
|
||||||
|
return hasText && !hasVideo && !hasAudio && !hasImage;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredData(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(section: "latest" | "popular") {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const typeId = typeMap[tipeKonten];
|
// Determine typeId based on contentType
|
||||||
const sortBy = urutan === "latest" ? "createdAt" : "viewCount";
|
const typeId = contentType === "all" ? 1 : parseInt(getTypeIdByContentType(contentType));
|
||||||
|
setCurrentTypeId(typeId.toString());
|
||||||
|
|
||||||
// 🔹 Panggil API baru
|
// 🔹 Ambil data artikel
|
||||||
const response = await listArticles(
|
const response = await listArticles(
|
||||||
1,
|
1,
|
||||||
20,
|
20,
|
||||||
typeId,
|
typeId, // Dynamic typeId based on content type
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
sortBy
|
// sortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasil: any[] = [];
|
let hasil: any[] = [];
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
console.error(
|
console.error("Articles API failed, fallback ke old API");
|
||||||
"Gagal ambil data dari listArticles, fallback ke listData"
|
const fallbackRes = await listData(
|
||||||
);
|
typeId.toString(),
|
||||||
const fallback = await listData(
|
|
||||||
String(typeId),
|
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
20,
|
20,
|
||||||
0,
|
0,
|
||||||
urutan === "latest" ? "createdAt" : "clickCount",
|
// urutan === "latest" ? "createdAt" : "clickCount",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
hasil = fallback?.data?.data?.content || [];
|
// hasil = fallback?.data?.data?.content || [];
|
||||||
} else {
|
} else {
|
||||||
hasil = response?.data?.data || [];
|
hasil = response?.data?.data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Normalisasi data artikel
|
// 🔹 Normalisasi data artikel
|
||||||
const dataBaru = hasil.map((a: any) => ({
|
const dataBaru = hasil.map((article: any) => ({
|
||||||
id: a.id,
|
id: article.id,
|
||||||
title: a.title,
|
title: article.title,
|
||||||
category:
|
category:
|
||||||
a.categoryName ||
|
article.categoryName ||
|
||||||
(a.categories && a.categories[0]?.title) ||
|
(article.categories && article.categories[0]?.title) ||
|
||||||
"Tanpa Kategori",
|
"Tanpa Kategori",
|
||||||
createdAt: a.createdAt,
|
createdAt: article.createdAt,
|
||||||
smallThumbnailLink: a.thumbnailUrl,
|
smallThumbnailLink: article.thumbnailUrl,
|
||||||
typeId: a.typeId,
|
label: article.categoryName,
|
||||||
|
...article,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setDataKonten(dataBaru);
|
// setDataKonten(dataBaru);
|
||||||
|
|
||||||
// 🔹 Sinkronisasi data bookmark
|
// 🔹 Sinkronisasi data bookmark
|
||||||
const roleId = Number(getCookiesDecrypt("urie"));
|
const roleId = Number(getCookiesDecrypt("urie"));
|
||||||
|
|
@ -204,25 +293,25 @@ export default function MediaUpdate() {
|
||||||
Media Update
|
Media Update
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* 🔸 Tab Urutan */}
|
{/* Main Tab */}
|
||||||
<div className="flex justify-center mb-8 bg-white">
|
<div className="flex justify-center mb-6 bg-white">
|
||||||
<Card className="bg-[#FFFFFF] rounded-xl flex flex-row p-3 gap-2">
|
<Card className="bg-[#FFFFFF] rounded-xl flex flex-row p-3 gap-2 shadow-md border border-gray-200">
|
||||||
<button
|
<button
|
||||||
onClick={() => setUrutan("latest")}
|
onClick={() => setTab("latest")}
|
||||||
className={`px-5 py-2 rounded-lg text-sm font-medium ${
|
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||||
urutan === "latest"
|
tab === "latest"
|
||||||
? "bg-[#C6A455] text-white"
|
? "bg-[#C6A455] text-white shadow-sm"
|
||||||
: "text-[#C6A455]"
|
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Terbaru
|
Terbaru
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setUrutan("popular")}
|
onClick={() => setTab("popular")}
|
||||||
className={`px-5 py-2 rounded-lg text-sm font-medium ${
|
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||||
urutan === "popular"
|
tab === "popular"
|
||||||
? "bg-[#C6A455] text-white"
|
? "bg-[#C6A455] text-white shadow-sm"
|
||||||
: "text-[#C6A455]"
|
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Terpopuler
|
Terpopuler
|
||||||
|
|
@ -230,26 +319,66 @@ export default function MediaUpdate() {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🔸 Tabs Tipe Konten */}
|
{/* Content Type Filter */}
|
||||||
<Tabs value={tipeKonten} onValueChange={(v: any) => setTipeKonten(v)}>
|
<div className="flex justify-center mb-8">
|
||||||
<TabsList className="flex mb-6 pb-2 bg-transparent">
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg">
|
||||||
{["image", "video", "text", "audio"].map((tipe) => (
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
<TabsTrigger
|
{/* <button
|
||||||
key={tipe}
|
onClick={() => setContentType("all")}
|
||||||
value={tipe}
|
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||||
className={`px-5 py-2 rounded-lg text-sm font-medium mx-1 border ${
|
contentType === "all"
|
||||||
tipeKonten === tipe
|
? "bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg ring-2 ring-blue-300"
|
||||||
? "bg-[#C6A455] text-white border-[#C6A455]"
|
: "bg-white text-blue-600 border-2 border-blue-200 hover:border-blue-400 hover:shadow-md"
|
||||||
: "text-[#C6A455] border-[#C6A455]"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tipe.charAt(0).toUpperCase() + tipe.slice(1)}
|
📋 Semua
|
||||||
</TabsTrigger>
|
</button> */}
|
||||||
))}
|
|
||||||
</TabsList>
|
<button
|
||||||
</Tabs>
|
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")}
|
||||||
|
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||||
|
contentType === "audiovisual"
|
||||||
|
? "bg-gradient-to-r from-purple-500 to-pink-600 text-white shadow-lg ring-2 ring-purple-300"
|
||||||
|
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🎬 Audio Visual
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setContentType("audio")}
|
||||||
|
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||||
|
contentType === "audio"
|
||||||
|
? "bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg ring-2 ring-green-300"
|
||||||
|
: "bg-white text-green-600 border-2 border-green-200 hover:border-green-400 hover:shadow-md"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🎵 Audio
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setContentType("text")}
|
||||||
|
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||||
|
contentType === "text"
|
||||||
|
? "bg-gradient-to-r from-gray-500 to-slate-600 text-white shadow-lg ring-2 ring-gray-300"
|
||||||
|
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📝 Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 🔸 Konten */}
|
{/* Slider */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-center">Memuat konten...</p>
|
<p className="text-center">Memuat konten...</p>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -263,32 +392,36 @@ export default function MediaUpdate() {
|
||||||
1024: { slidesPerView: 4 },
|
1024: { slidesPerView: 4 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{dataKonten.map((item) => (
|
{filteredData.map((item) => (
|
||||||
<SwiperSlide key={item.id}>
|
<SwiperSlide key={item.id}>
|
||||||
<div className="rounded-xl shadow-md overflow-hidden bg-white">
|
<div className="rounded-xl shadow-md overflow-hidden bg-white">
|
||||||
<div className="w-full h-[204px] relative">
|
<div className="w-full h-[204px] relative">
|
||||||
<Image
|
<Link href={getLink(item)}>
|
||||||
src={item.smallThumbnailLink || "/placeholder.png"}
|
<Image
|
||||||
alt={item.title || "No Title"}
|
src={item.smallThumbnailLink || "/placeholder.png"}
|
||||||
fill
|
alt={item.title || "No Title"}
|
||||||
className="object-cover"
|
fill
|
||||||
/>
|
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="flex gap-2 mb-1">
|
<div className="flex gap-2 mb-1">
|
||||||
<span className="text-xs text-white px-2 py-0.5 rounded bg-blue-600">
|
<span className="text-xs text-white px-2 py-0.5 rounded bg-blue-600">
|
||||||
{item.category || "Tanpa Kategori"}
|
{item.clientName || "Tanpa Kategori"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium text-[#b3882e] capitalize">
|
{/* <span className="text-xs font-medium text-[#b3882e] capitalize">
|
||||||
{tipeKonten}
|
{tipeKonten}
|
||||||
</span>
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mb-1">
|
<p className="text-xs text-gray-500 mb-1">
|
||||||
{formatTanggal(item.createdAt)}
|
{formatTanggal(item.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-semibold mb-3 line-clamp-2">
|
<Link href={getLink(item)}>
|
||||||
{item.title}
|
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
|
||||||
</p>
|
{item.title}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2 text-gray-600">
|
<div className="flex gap-2 text-gray-600">
|
||||||
|
|
@ -318,335 +451,22 @@ export default function MediaUpdate() {
|
||||||
</Swiper>
|
</Swiper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 🔸 Tombol Lihat Lebih Banyak */}
|
{/* Lihat lebih banyak - hanya muncul jika ada data */}
|
||||||
<div className="text-center mt-10">
|
{filteredData.length > 0 && (
|
||||||
<Link
|
<div className="text-center mt-10">
|
||||||
href={`/${tipeKonten}/filter?sortBy=${urutan}`}
|
<Link
|
||||||
className="inline-block border border-[#b3882e] text-[#b3882e] px-6 py-2 rounded-md text-sm font-medium hover:bg-[#b3882e]/10 transition"
|
href={getContentTypeLink()}
|
||||||
>
|
>
|
||||||
Lihat Lebih Banyak
|
<Button
|
||||||
</Link>
|
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>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "use client";
|
|
||||||
|
|
||||||
// import { useState, useEffect } from "react";
|
|
||||||
// import Image from "next/image";
|
|
||||||
// import { Button } from "@/components/ui/button";
|
|
||||||
// import { ThumbsUp, ThumbsDown } from "lucide-react";
|
|
||||||
// import { Card } from "../ui/card";
|
|
||||||
// import Link from "next/link";
|
|
||||||
// import { listData, listArticles } from "@/service/landing/landing";
|
|
||||||
// import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
|
|
||||||
// import { getCookiesDecrypt } from "@/lib/utils";
|
|
||||||
// import { Swiper, SwiperSlide } from "swiper/react";
|
|
||||||
// import "swiper/css";
|
|
||||||
// import "swiper/css/navigation";
|
|
||||||
// import { Navigation } from "swiper/modules";
|
|
||||||
// import Swal from "sweetalert2";
|
|
||||||
// import withReactContent from "sweetalert2-react-content";
|
|
||||||
|
|
||||||
// // 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"
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default function MediaUpdate() {
|
|
||||||
// const [tab, setTab] = useState<"latest" | "popular">("latest");
|
|
||||||
// const [dataToRender, setDataToRender] = useState<any[]>([]);
|
|
||||||
// const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
|
||||||
// const [loading, setLoading] = useState(true);
|
|
||||||
// const MySwal = withReactContent(Swal);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// fetchData(tab);
|
|
||||||
// }, [tab]);
|
|
||||||
|
|
||||||
// async function fetchData(section: "latest" | "popular") {
|
|
||||||
// try {
|
|
||||||
// setLoading(true);
|
|
||||||
|
|
||||||
// const response = await listArticles(
|
|
||||||
// 1,
|
|
||||||
// 20,
|
|
||||||
// 1, // typeId = image
|
|
||||||
// undefined,
|
|
||||||
// undefined,
|
|
||||||
// section === "latest" ? "createdAt" : "viewCount"
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let articlesData: any[] = [];
|
|
||||||
|
|
||||||
// if (response?.error) {
|
|
||||||
// console.error("Articles API failed, fallback ke old API");
|
|
||||||
// const fallbackRes = await listData(
|
|
||||||
// "1",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// 20,
|
|
||||||
// 0,
|
|
||||||
// section === "latest" ? "createdAt" : "clickCount",
|
|
||||||
// "",
|
|
||||||
// "",
|
|
||||||
// ""
|
|
||||||
// );
|
|
||||||
// articlesData = fallbackRes?.data?.data?.content || [];
|
|
||||||
// } else {
|
|
||||||
// articlesData = response?.data?.data || [];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 🔹 Normalisasi struktur data
|
|
||||||
// const transformedData = articlesData.map((article: any) => ({
|
|
||||||
// id: article.id,
|
|
||||||
// title: article.title,
|
|
||||||
// category:
|
|
||||||
// article.categoryName ||
|
|
||||||
// (article.categories && article.categories[0]?.title) ||
|
|
||||||
// "Tanpa Kategori",
|
|
||||||
// createdAt: article.createdAt,
|
|
||||||
// smallThumbnailLink: article.thumbnailUrl,
|
|
||||||
// label:
|
|
||||||
// article.typeId === 1
|
|
||||||
// ? "Image"
|
|
||||||
// : article.typeId === 2
|
|
||||||
// ? "Video"
|
|
||||||
// : article.typeId === 3
|
|
||||||
// ? "Text"
|
|
||||||
// : article.typeId === 4
|
|
||||||
// ? "Audio"
|
|
||||||
// : "",
|
|
||||||
// ...article,
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// setDataToRender(transformedData);
|
|
||||||
|
|
||||||
// // 🔹 Sinkronisasi bookmark
|
|
||||||
// const roleId = Number(getCookiesDecrypt("urie"));
|
|
||||||
// if (roleId && !isNaN(roleId)) {
|
|
||||||
// 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 (err) {
|
|
||||||
// console.error("Gagal memuat data:", err);
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 🔹 Simpan perubahan bookmark ke localStorage
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (bookmarkedIds.size > 0) {
|
|
||||||
// localStorage.setItem(
|
|
||||||
// "bookmarkedIds",
|
|
||||||
// JSON.stringify(Array.from(bookmarkedIds))
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }, [bookmarkedIds]);
|
|
||||||
|
|
||||||
// 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.",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <section className="bg-white px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
|
|
||||||
// <div className="max-w-screen-xl mx-auto">
|
|
||||||
// <h2 className="text-2xl font-semibold text-center mb-6">
|
|
||||||
// Media Update
|
|
||||||
// </h2>
|
|
||||||
|
|
||||||
// {/* Tab */}
|
|
||||||
// <div className="flex justify-center mb-8 bg-white">
|
|
||||||
// <Card className="bg-[#FFFFFF] rounded-xl flex flex-row p-3 gap-2">
|
|
||||||
// <button
|
|
||||||
// onClick={() => setTab("latest")}
|
|
||||||
// className={`px-5 py-2 rounded-lg text-sm font-medium ${
|
|
||||||
// tab === "latest" ? "bg-[#C6A455] text-white" : "text-[#C6A455]"
|
|
||||||
// }`}
|
|
||||||
// >
|
|
||||||
// Konten Terbaru
|
|
||||||
// </button>
|
|
||||||
// <button
|
|
||||||
// onClick={() => setTab("popular")}
|
|
||||||
// className={`px-5 py-2 rounded-lg text-sm font-medium ${
|
|
||||||
// tab === "popular" ? "bg-[#C6A455] text-white" : "text-[#C6A455]"
|
|
||||||
// }`}
|
|
||||||
// >
|
|
||||||
// Konten Terpopuler
|
|
||||||
// </button>
|
|
||||||
// </Card>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* Slider */}
|
|
||||||
// {loading ? (
|
|
||||||
// <p className="text-center">Loading...</p>
|
|
||||||
// ) : (
|
|
||||||
// <Swiper
|
|
||||||
// modules={[Navigation]}
|
|
||||||
// navigation
|
|
||||||
// spaceBetween={20}
|
|
||||||
// slidesPerView={1}
|
|
||||||
// breakpoints={{
|
|
||||||
// 640: { slidesPerView: 2 },
|
|
||||||
// 1024: { slidesPerView: 4 },
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {dataToRender.map((item) => (
|
|
||||||
// <SwiperSlide key={item.id}>
|
|
||||||
// <div className="rounded-xl shadow-md overflow-hidden bg-white">
|
|
||||||
// <div className="w-full h-[204px] relative">
|
|
||||||
// <Image
|
|
||||||
// src={item.smallThumbnailLink || "/placeholder.png"}
|
|
||||||
// alt={item.title || "No Title"}
|
|
||||||
// fill
|
|
||||||
// className="object-cover"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// <div className="p-3">
|
|
||||||
// <div className="flex gap-2 mb-1">
|
|
||||||
// <span className="text-xs text-white px-2 py-0.5 rounded bg-blue-600">
|
|
||||||
// {item.category || "Tanpa Kategori"}
|
|
||||||
// </span>
|
|
||||||
// <span className="text-xs font-medium text-[#b3882e]">
|
|
||||||
// {item.label || ""}
|
|
||||||
// </span>
|
|
||||||
// </div>
|
|
||||||
// <p className="text-xs text-gray-500 mb-1">
|
|
||||||
// {formatTanggal(item.createdAt)}
|
|
||||||
// </p>
|
|
||||||
// <p className="text-sm font-semibold mb-3 line-clamp-2">
|
|
||||||
// {item.title}
|
|
||||||
// </p>
|
|
||||||
|
|
||||||
// <div className="flex items-center justify-between">
|
|
||||||
// <div className="flex gap-2 text-gray-600">
|
|
||||||
// <ThumbsUp className="w-4 h-4 cursor-pointer" />
|
|
||||||
// <ThumbsDown className="w-4 h-4 cursor-pointer" />
|
|
||||||
// </div>
|
|
||||||
// <Button
|
|
||||||
// onClick={() => handleSave(item.id)}
|
|
||||||
// disabled={bookmarkedIds.has(Number(item.id))}
|
|
||||||
// variant="default"
|
|
||||||
// size="sm"
|
|
||||||
// className={`rounded px-4 ${
|
|
||||||
// bookmarkedIds.has(Number(item.id))
|
|
||||||
// ? "bg-gray-400 cursor-not-allowed text-white"
|
|
||||||
// : "bg-blue-600 text-white hover:bg-blue-700"
|
|
||||||
// }`}
|
|
||||||
// >
|
|
||||||
// {bookmarkedIds.has(Number(item.id)) ? "Saved" : "Save"}
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </SwiperSlide>
|
|
||||||
// ))}
|
|
||||||
// </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
|
|
||||||
// </Button>
|
|
||||||
// </Link>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </section>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook untuk mengelola state dengan localStorage
|
||||||
|
* @param key - Key untuk localStorage
|
||||||
|
* @param initialValue - Nilai awal jika tidak ada data di localStorage
|
||||||
|
* @returns [value, setValue] - Tuple berisi nilai dan fungsi setter
|
||||||
|
*/
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
||||||
|
// Fungsi untuk mendapatkan nilai dari localStorage
|
||||||
|
const getStoredValue = (): T => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
return item ? JSON.parse(item) : initialValue;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error reading localStorage key "${key}":`, error);
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// State dengan lazy initialization
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(getStoredValue);
|
||||||
|
|
||||||
|
// Fungsi setter yang juga menyimpan ke localStorage
|
||||||
|
const setValue = (value: T) => {
|
||||||
|
try {
|
||||||
|
setStoredValue(value);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error setting localStorage key "${key}":`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update state jika localStorage berubah dari tab lain
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStorageChange = (e: StorageEvent) => {
|
||||||
|
if (e.key === key && e.newValue !== null) {
|
||||||
|
try {
|
||||||
|
setStoredValue(JSON.parse(e.newValue));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error parsing localStorage value for key "${key}":`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
return () => window.removeEventListener('storage', handleStorageChange);
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sweetalert2": "^11.10.5",
|
"sweetalert2": "^11.10.5",
|
||||||
"sweetalert2-react-content": "^5.1.0",
|
"sweetalert2-react-content": "^5.1.0",
|
||||||
"swiper": "^11.1.15",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
|
|
@ -21233,7 +21233,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swiper": {
|
"node_modules/swiper": {
|
||||||
"version": "11.1.15",
|
"version": "11.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.10.tgz",
|
||||||
|
"integrity": "sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "patreon",
|
"type": "patreon",
|
||||||
|
|
@ -21244,7 +21246,6 @@
|
||||||
"url": "http://opencollective.com/swiper"
|
"url": "http://opencollective.com/swiper"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.7.0"
|
"node": ">= 4.7.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sweetalert2": "^11.10.5",
|
"sweetalert2": "^11.10.5",
|
||||||
"sweetalert2-react-content": "^5.1.0",
|
"sweetalert2-react-content": "^5.1.0",
|
||||||
"swiper": "^11.1.15",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
} from "../http-config/http-interceptor-service";
|
||||||
|
|
||||||
|
// Types untuk Article Category
|
||||||
|
export interface ArticleCategory {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
slug: string;
|
||||||
|
tags: string[];
|
||||||
|
thumbnailPath: string | null;
|
||||||
|
parentId: number | null;
|
||||||
|
oldCategoryId: number | null;
|
||||||
|
createdById: number;
|
||||||
|
statusId: number;
|
||||||
|
isPublish: boolean;
|
||||||
|
publishedAt: string | null;
|
||||||
|
isEnabled: boolean | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleCategoriesResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
messages: string[];
|
||||||
|
data: ArticleCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service function
|
||||||
|
export async function getArticleCategories() {
|
||||||
|
const url = "/article-categories";
|
||||||
|
return httpGetInterceptor(url);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
} from "../http-config/http-interceptor-service";
|
||||||
|
|
||||||
|
// Types untuk Client Profile
|
||||||
|
export interface ClientProfile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
clientType: string;
|
||||||
|
parentClientId: string | null;
|
||||||
|
logoUrl: string | null;
|
||||||
|
logoImagePath: string | null;
|
||||||
|
address: string | null;
|
||||||
|
phoneNumber: string | null;
|
||||||
|
website: string | null;
|
||||||
|
subClientCount: number;
|
||||||
|
maxUsers: number | null;
|
||||||
|
maxStorage: number | null;
|
||||||
|
currentUsers: number;
|
||||||
|
currentStorage: number | null;
|
||||||
|
settings: any | null;
|
||||||
|
createdById: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateClientProfileRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
address?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
website?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service functions
|
||||||
|
export async function getClientProfile() {
|
||||||
|
const url = "/clients/profile";
|
||||||
|
return httpGetInterceptor(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateClientProfile(data: UpdateClientProfileRequest) {
|
||||||
|
const url = "/clients/update";
|
||||||
|
return httpPutInterceptor(url, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadClientLogo(logoFile: File) {
|
||||||
|
const url = "/clients/logo";
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("logo", logoFile);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
|
||||||
|
return httpPostInterceptor(url, formData, headers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
} from "../http-config/http-interceptor-service";
|
||||||
|
|
||||||
|
// Types untuk Public Client
|
||||||
|
export interface PublicClient {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string | null;
|
||||||
|
clientType: string;
|
||||||
|
logoUrl: string | null;
|
||||||
|
address: string | null;
|
||||||
|
phoneNumber: string | null;
|
||||||
|
website: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicClientsResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
messages: string[];
|
||||||
|
data: PublicClient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service function
|
||||||
|
export async function getPublicClients() {
|
||||||
|
const url = "/clients/public";
|
||||||
|
return httpGetInterceptor(url);
|
||||||
|
}
|
||||||
|
|
@ -171,13 +171,13 @@ export async function listData(
|
||||||
// New Articles API for public/landing usage
|
// New Articles API for public/landing usage
|
||||||
export async function listArticles(
|
export async function listArticles(
|
||||||
page = 1,
|
page = 1,
|
||||||
totalPage = 10,
|
limit = 10,
|
||||||
typeId?: number,
|
typeId?: number,
|
||||||
search?: string,
|
search?: string,
|
||||||
categoryId?: string,
|
categoryId?: string,
|
||||||
sortBy = "createdAt"
|
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 (typeId !== undefined) url += `&typeId=${typeId}`;
|
||||||
if (search) url += `&title=${encodeURIComponent(search)}`;
|
if (search) url += `&title=${encodeURIComponent(search)}`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue