diff --git a/app/[locale]/(admin)/admin/settings/tenant/component/tenant-settings-content-table.tsx b/app/[locale]/(admin)/admin/settings/tenant/component/tenant-settings-content-table.tsx index a9beac7..94dda41 100644 --- a/app/[locale]/(admin)/admin/settings/tenant/component/tenant-settings-content-table.tsx +++ b/app/[locale]/(admin)/admin/settings/tenant/component/tenant-settings-content-table.tsx @@ -26,6 +26,7 @@ import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm"; import { UserLevelsForm } from "@/components/form/UserLevelsForm"; import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider"; import { useWorkflowStatusCheck } from "@/hooks/useWorkflowStatusCheck"; +import { useLocalStorage } from "@/hooks/use-local-storage"; import { CreateApprovalWorkflowWithClientSettingsRequest, UserLevelsCreateRequest, @@ -60,7 +61,7 @@ import { close, loading } from "@/config/swal"; import DetailTenant from "@/components/form/tenant/tenant-detail-update-form"; function TenantSettingsContentTable() { - const [activeTab, setActiveTab] = useState("workflows"); + const [activeTab, setActiveTab] = useLocalStorage('tenant-settings-active-tab', 'profile'); const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false); const [workflow, setWorkflow] = useState(null); @@ -220,7 +221,7 @@ function TenantSettingsContentTable() { @@ -243,7 +244,7 @@ function TenantSettingsContentTable() { {/* Approval Workflows Tab */} - + diff --git a/app/[locale]/(public)/content/audio/page.tsx b/app/[locale]/(public)/content/audio/page.tsx new file mode 100644 index 0000000..b228ded --- /dev/null +++ b/app/[locale]/(public)/content/audio/page.tsx @@ -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([]); + const [categories, setCategories] = useState([]); + const [bookmarkedIds, setBookmarkedIds] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [selectedType, setSelectedType] = useState("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(); + 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( + (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 ( +
+
+
+ {/* Filter Sidebar */} +
+ +
+
+ +

Filter Artikel

+
+ + {/* Search */} +
+ +
+ setSearchTerm(e.target.value)} + className="flex-1" + /> + +
+
+ + {/* Category Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Date Filter */} +
+ +
+ { + setStartDate(e.target.value); + handleFilterChange(); + }} + /> + { + setEndDate(e.target.value); + handleFilterChange(); + }} + /> +
+
+ + {/* Clear Filters */} + +
+
+
+ + {/* Main Content */} +
+
+

Daftar Artikel

+

+ Menampilkan {totalData} artikel + {searchTerm && ` untuk "${searchTerm}"`} + {selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`} +

+
+ + {/* Articles Grid */} + {loading ? ( +
+ {[...Array(6)].map((_, i) => ( + +
+
+
+
+
+
+
+ ))} +
+ ) : articles.length > 0 ? ( +
+ {articles.map((article) => ( + +
+ + {article.title + +
+
+
+ + {article.clientName} + + + {article.categoryName || "Tanpa Kategori"} + +
+

+ {formatTanggal(article.createdAt)} +

+ +

+ {article.title} +

+ + {/*

+ {article.description || article.content || ""} +

*/} + +
+
+ + +
+ +
+
+
+ ))} +
+ ) : ( + +
+

Tidak ada artikel ditemukan

+

Coba ubah filter atau kata kunci pencarian

+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+ + + + handlePageChange(currentPage - 1)} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {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 ( + + handlePageChange(pageNumber)} + isActive={currentPage === pageNumber} + className="cursor-pointer" + > + {pageNumber} + + + ); + })} + + + handlePageChange(currentPage + 1)} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+
+
+ ); +} diff --git a/app/[locale]/(public)/content/image/page.tsx b/app/[locale]/(public)/content/image/page.tsx new file mode 100644 index 0000000..ac14c46 --- /dev/null +++ b/app/[locale]/(public)/content/image/page.tsx @@ -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([]); + const [categories, setCategories] = useState([]); + const [bookmarkedIds, setBookmarkedIds] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [selectedType, setSelectedType] = useState("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(); + 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( + (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 ( +
+
+
+ {/* Filter Sidebar */} +
+ +
+
+ +

Filter Artikel

+
+ + {/* Search */} +
+ +
+ setSearchTerm(e.target.value)} + className="flex-1" + /> + +
+
+ + {/* Category Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Date Filter */} +
+ +
+ { + setStartDate(e.target.value); + handleFilterChange(); + }} + /> + { + setEndDate(e.target.value); + handleFilterChange(); + }} + /> +
+
+ + {/* Clear Filters */} + +
+
+
+ + {/* Main Content */} +
+
+

Daftar Artikel

+

+ Menampilkan {totalData} artikel + {searchTerm && ` untuk "${searchTerm}"`} + {selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`} +

+
+ + {/* Articles Grid */} + {loading ? ( +
+ {[...Array(6)].map((_, i) => ( + +
+
+
+
+
+
+
+ ))} +
+ ) : articles.length > 0 ? ( +
+ {articles.map((article) => ( + +
+ + {article.title + +
+
+
+ + {article.clientName} + + + {article.categoryName || "Tanpa Kategori"} + +
+

+ {formatTanggal(article.createdAt)} +

+ +

+ {article.title} +

+ + {/*

+ {article.description || article.content || ""} +

*/} + +
+
+ + +
+ +
+
+
+ ))} +
+ ) : ( + +
+

Tidak ada artikel ditemukan

+

Coba ubah filter atau kata kunci pencarian

+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+ + + + handlePageChange(currentPage - 1)} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {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 ( + + handlePageChange(pageNumber)} + isActive={currentPage === pageNumber} + className="cursor-pointer" + > + {pageNumber} + + + ); + })} + + + handlePageChange(currentPage + 1)} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+
+
+ ); +} diff --git a/app/[locale]/(public)/content/text/page.tsx b/app/[locale]/(public)/content/text/page.tsx new file mode 100644 index 0000000..7380a0b --- /dev/null +++ b/app/[locale]/(public)/content/text/page.tsx @@ -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([]); + const [categories, setCategories] = useState([]); + const [bookmarkedIds, setBookmarkedIds] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [selectedType, setSelectedType] = useState("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(); + 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( + (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 ( +
+
+
+ {/* Filter Sidebar */} +
+ +
+
+ +

Filter Artikel

+
+ + {/* Search */} +
+ +
+ setSearchTerm(e.target.value)} + className="flex-1" + /> + +
+
+ + {/* Category Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Date Filter */} +
+ +
+ { + setStartDate(e.target.value); + handleFilterChange(); + }} + /> + { + setEndDate(e.target.value); + handleFilterChange(); + }} + /> +
+
+ + {/* Clear Filters */} + +
+
+
+ + {/* Main Content */} +
+
+

Daftar Artikel

+

+ Menampilkan {totalData} artikel + {searchTerm && ` untuk "${searchTerm}"`} + {selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`} +

+
+ + {/* Articles Grid */} + {loading ? ( +
+ {[...Array(6)].map((_, i) => ( + +
+
+
+
+
+
+
+ ))} +
+ ) : articles.length > 0 ? ( +
+ {articles.map((article) => ( + +
+ + {article.title + +
+
+
+ + {article.clientName} + + + {article.categoryName || "Tanpa Kategori"} + +
+

+ {formatTanggal(article.createdAt)} +

+ +

+ {article.title} +

+ + {/*

+ {article.description || article.content || ""} +

*/} + +
+
+ + +
+ +
+
+
+ ))} +
+ ) : ( + +
+

Tidak ada artikel ditemukan

+

Coba ubah filter atau kata kunci pencarian

+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+ + + + handlePageChange(currentPage - 1)} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {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 ( + + handlePageChange(pageNumber)} + isActive={currentPage === pageNumber} + className="cursor-pointer" + > + {pageNumber} + + + ); + })} + + + handlePageChange(currentPage + 1)} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+
+
+ ); +} diff --git a/app/[locale]/(public)/content/video/page.tsx b/app/[locale]/(public)/content/video/page.tsx new file mode 100644 index 0000000..677fed3 --- /dev/null +++ b/app/[locale]/(public)/content/video/page.tsx @@ -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([]); + const [categories, setCategories] = useState([]); + const [bookmarkedIds, setBookmarkedIds] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [selectedType, setSelectedType] = useState("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(); + 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( + (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 ( +
+
+
+ {/* Filter Sidebar */} +
+ +
+
+ +

Filter Artikel

+
+ + {/* Search */} +
+ +
+ setSearchTerm(e.target.value)} + className="flex-1" + /> + +
+
+ + {/* Category Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Date Filter */} +
+ +
+ { + setStartDate(e.target.value); + handleFilterChange(); + }} + /> + { + setEndDate(e.target.value); + handleFilterChange(); + }} + /> +
+
+ + {/* Clear Filters */} + +
+
+
+ + {/* Main Content */} +
+
+

Daftar Artikel

+

+ Menampilkan {totalData} artikel + {searchTerm && ` untuk "${searchTerm}"`} + {selectedCategory !== "all" && ` dalam kategori "${categories.find(c => c.id === parseInt(selectedCategory))?.title || selectedCategory}"`} +

+
+ + {/* Articles Grid */} + {loading ? ( +
+ {[...Array(6)].map((_, i) => ( + +
+
+
+
+
+
+
+ ))} +
+ ) : articles.length > 0 ? ( +
+ {articles.map((article) => ( + +
+ + {article.title + +
+
+
+ + {article.clientName} + + + {article.categoryName || "Tanpa Kategori"} + +
+

+ {formatTanggal(article.createdAt)} +

+ +

+ {article.title} +

+ + {/*

+ {article.description || article.content || ""} +

*/} + +
+
+ + +
+ +
+
+
+ ))} +
+ ) : ( + +
+

Tidak ada artikel ditemukan

+

Coba ubah filter atau kata kunci pencarian

+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+ + + + handlePageChange(currentPage - 1)} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {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 ( + + handlePageChange(pageNumber)} + isActive={currentPage === pageNumber} + className="cursor-pointer" + > + {pageNumber} + + + ); + })} + + + handlePageChange(currentPage + 1)} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+
+
+ ); +} diff --git a/app/[locale]/(public)/(tenant)/tenant/[tenant-name]/layout.tsx b/app/[locale]/(public)/tenant/[tenant-name]/layout.tsx similarity index 100% rename from app/[locale]/(public)/(tenant)/tenant/[tenant-name]/layout.tsx rename to app/[locale]/(public)/tenant/[tenant-name]/layout.tsx diff --git a/app/[locale]/(public)/(tenant)/tenant/[tenant-name]/page.tsx b/app/[locale]/(public)/tenant/[tenant-name]/page.tsx similarity index 100% rename from app/[locale]/(public)/(tenant)/tenant/[tenant-name]/page.tsx rename to app/[locale]/(public)/tenant/[tenant-name]/page.tsx diff --git a/components/form/ApprovalWorkflowForm.tsx b/components/form/ApprovalWorkflowForm.tsx index 23974fd..a2f5eb7 100644 --- a/components/form/ApprovalWorkflowForm.tsx +++ b/components/form/ApprovalWorkflowForm.tsx @@ -687,6 +687,7 @@ export const ApprovalWorkflowForm: React.FC = ({ + )} )} +

+ Format yang didukung: JPG, PNG, WebP. Maksimal 5MB. +

{/* Nama Perusahaan */} @@ -278,7 +340,7 @@ export default function TenantCompanyUpdateForm({ {/* Status Aktif */} -
+ {/*
-
+
*/} - + diff --git a/components/landing-page/category.tsx b/components/landing-page/category.tsx index 563e700..f9d47e5 100644 --- a/components/landing-page/category.tsx +++ b/components/landing-page/category.tsx @@ -1,7 +1,38 @@ "use client"; +import { useState, useEffect } from "react"; +import { getArticleCategories, ArticleCategory } from "@/service/categories/article-categories"; + export default function Category() { - const categories = [ + const [categories, setCategories] = useState([]); + 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", "OPERASI KETUPAT 2025", "HUT HUMAS KE-74", @@ -14,22 +45,50 @@ export default function Category() { "SEPUTAR PRESTASI", ]; + const displayCategories = categories.length > 0 ? categories : fallbackCategories; + return (

- 10 Kategori Paling Populer + {loading ? "Memuat Kategori..." : `${displayCategories.length} Kategori Paling Populer`}

-
- {categories.map((category, index) => ( - - ))} -
+ + {loading ? ( + // Loading skeleton +
+ {Array.from({ length: 10 }).map((_, index) => ( +
+
+
+ ))} +
+ ) : ( +
+ {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 ( + + ); + })} +
+ )}
); diff --git a/components/landing-page/footer.tsx b/components/landing-page/footer.tsx index 02cc520..41939be 100644 --- a/components/landing-page/footer.tsx +++ b/components/landing-page/footer.tsx @@ -1,8 +1,52 @@ "use client"; -import { Instagram, ChevronLeft, ChevronRight } from "lucide-react"; +import { Instagram } from "lucide-react"; 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 = [ // { src: "/mabes.png", href: "/in/public/publication/kl" }, @@ -27,63 +71,120 @@ const logos = [ ]; export default function Footer() { - const scrollRef = useRef(null); + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(true); - const scroll = (direction: "left" | "right") => { - if (scrollRef.current) { - scrollRef.current.scrollBy({ - left: direction === "left" ? -200 : 200, - behavior: "smooth", - }); + // Fetch public clients + useEffect(() => { + async function fetchClients() { + try { + 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 (