Merge branch 'main' of https://gitlab.com/hanifsalafi/new-netidhub-public into dev-1
This commit is contained in:
commit
c003ef075e
|
|
@ -1,39 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getPublicClients, PublicClient } from "@/service/client/public-clients";
|
||||
|
||||
type CategoryTabsProps = {
|
||||
selectedCategory: string;
|
||||
onCategoryChange: (category: string) => void;
|
||||
};
|
||||
|
||||
const categories = [
|
||||
"SEMUA",
|
||||
"POLRI",
|
||||
"MAHKAMAH AGUNG",
|
||||
"DPR",
|
||||
"MPR",
|
||||
"KEJAKSAAN AGUNG",
|
||||
"KPK",
|
||||
"PUPR",
|
||||
"BSKDN",
|
||||
"BUMN",
|
||||
"KPU",
|
||||
];
|
||||
|
||||
export default function CategoryTabs({
|
||||
selectedCategory,
|
||||
onCategoryChange,
|
||||
}: CategoryTabsProps) {
|
||||
const [clients, setClients] = useState<PublicClient[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 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 empty array if API fails
|
||||
setClients([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchClients();
|
||||
}, []);
|
||||
|
||||
// Create categories array with "SEMUA" first, then client names
|
||||
const categories = ["SEMUA", ...clients.map(client => client.name)];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 overflow-x-auto">
|
||||
{Array.from({ length: 6 }).map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="px-4 py-1 text-sm rounded font-medium border bg-gray-200 animate-pulse h-8 w-20"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 overflow-x-auto">
|
||||
{categories.map((cat, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onCategoryChange(cat)}
|
||||
className={`px-4 py-1 text-sm rounded font-medium border ${
|
||||
className={`px-4 py-1 text-sm rounded font-medium border transition-colors ${
|
||||
selectedCategory === cat
|
||||
? "bg-[#C6A455] text-white"
|
||||
: "bg-white text-gray-800"
|
||||
: "bg-white text-gray-800 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
{cat.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,15 +12,16 @@ export default function PublicationKlLayout() {
|
|||
const [selectedCategory, setSelectedCategory] = useState("SEMUA");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const roleId = getCookiesDecrypt("urie") ?? "";
|
||||
// useEffect(() => {
|
||||
// const roleId = getCookiesDecrypt("urie") ?? "";
|
||||
|
||||
const allowedRoles = ["6", "7", "2", "3"];
|
||||
// const allowedRoles = ["6", "7", "2", "3"];
|
||||
|
||||
// if (!allowedRoles.includes(roleId)) {
|
||||
// router.push("/auth");
|
||||
// }
|
||||
// }, [router]);
|
||||
|
||||
if (!allowedRoles.includes(roleId)) {
|
||||
router.push("/auth");
|
||||
}
|
||||
}, [router]);
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
|
||||
<CategoryTabs
|
||||
|
|
|
|||
|
|
@ -1,12 +1,64 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import PublicationKlFilter from "./publication-filter";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThumbsUp, ThumbsDown, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getBookmarks, toggleBookmarkById, BookmarkItem } from "@/service/content";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import { getBookmarksByUserId, getBookmarksForUser } from "@/service/landing/landing";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
const itemsPerPage = 9;
|
||||
// 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: BookmarkItem) {
|
||||
switch (item.article?.typeId) {
|
||||
case 1:
|
||||
return `/content/image/detail/${item.article?.id}`;
|
||||
case 2:
|
||||
return `/content/video/detail/${item.article?.id}`;
|
||||
case 3:
|
||||
return `/content/text/detail/${item.article?.id}`;
|
||||
case 4:
|
||||
return `/content/audio/detail/${item.article?.id}`;
|
||||
default:
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get content type label
|
||||
function getContentTypeLabel(typeId: number) {
|
||||
switch (typeId) {
|
||||
case 1:
|
||||
return "📸 Image";
|
||||
case 2:
|
||||
return "🎬 Video";
|
||||
case 3:
|
||||
return "📝 Text";
|
||||
case 4:
|
||||
return "🎵 Audio";
|
||||
default:
|
||||
return "📄 Content";
|
||||
}
|
||||
}
|
||||
|
||||
type PublicationCardGridProps = {
|
||||
selectedCategory: string;
|
||||
|
|
@ -26,80 +78,187 @@ export default function ForYouCardGrid({
|
|||
isInstitute = false,
|
||||
instituteId = "",
|
||||
}: PublicationCardGridProps) {
|
||||
const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [contentImage, setContentImage] = useState<any[]>([]);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [categoryFilter] = useState<string[]>([]);
|
||||
const [formatFilter] = useState<string[]>([]);
|
||||
const [sortBy] = useState<string>("createdAt");
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [limit] = useState(12);
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
useEffect(() => {
|
||||
getDataBookmark();
|
||||
fetchBookmarks();
|
||||
}, [currentPage, selectedCategory, title, refresh]);
|
||||
|
||||
async function getDataBookmark() {
|
||||
console.log("📡 Fetching bookmark list...");
|
||||
|
||||
const fetchBookmarks = async () => {
|
||||
try {
|
||||
const response = await getBookmarksForUser(1, 12, "desc", "createdAt");
|
||||
console.log("✅ Bookmark response:", response);
|
||||
setLoading(true);
|
||||
const response = await getBookmarks(currentPage, limit);
|
||||
|
||||
const bookmarks =
|
||||
response?.data?.data?.content || response?.data?.data || [];
|
||||
const totalPage = response?.data?.data?.totalPages || 1;
|
||||
|
||||
setContentImage(bookmarks);
|
||||
setTotalPages(totalPage);
|
||||
} catch (error) {
|
||||
console.error("❌ Gagal memuat bookmark:", error);
|
||||
setContentImage([]);
|
||||
if (!response?.error) {
|
||||
setBookmarks(response.data?.data || []);
|
||||
setTotalPages(response.data?.meta?.totalPage || 1);
|
||||
setTotalCount(response.data?.meta?.count || 0);
|
||||
} else {
|
||||
console.error("Failed to fetch bookmarks:", response?.error);
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Gagal memuat bookmark.",
|
||||
confirmButtonColor: "#d33",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching bookmarks:", err);
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Terjadi kesalahan saat memuat bookmark.",
|
||||
confirmButtonColor: "#d33",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBookmark = async (bookmarkId: number, articleId: number, articleTitle: string) => {
|
||||
const result = await MySwal.fire({
|
||||
title: "Hapus Bookmark",
|
||||
text: `Apakah Anda yakin ingin menghapus "${articleTitle}" dari bookmark?`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Ya, Hapus!",
|
||||
cancelButtonText: "Batal",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await toggleBookmarkById(articleId);
|
||||
|
||||
if (response?.data?.success || !response?.error) {
|
||||
// Remove from local state
|
||||
setBookmarks(prev => prev.filter(bookmark => bookmark.id !== bookmarkId));
|
||||
setTotalCount(prev => prev - 1);
|
||||
|
||||
MySwal.fire({
|
||||
icon: "success",
|
||||
title: "Berhasil",
|
||||
text: "Bookmark berhasil dihapus.",
|
||||
timer: 1500,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
} else {
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal",
|
||||
text: "Gagal menghapus bookmark.",
|
||||
confirmButtonColor: "#d33",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error deleting bookmark:", err);
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Terjadi kesalahan saat menghapus bookmark.",
|
||||
confirmButtonColor: "#d33",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="rounded-xl shadow-md overflow-hidden bg-white animate-pulse">
|
||||
<div className="w-full h-[204px] bg-gray-200"></div>
|
||||
<div className="p-3 space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (bookmarks.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-6xl mb-4">📚</div>
|
||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">Tidak Ada Bookmark</h3>
|
||||
<p className="text-gray-500">Anda belum menyimpan artikel apapun ke bookmark.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-800 mb-2">Bookmark Saya</h2>
|
||||
<p className="text-gray-600">Total {totalCount} artikel tersimpan</p>
|
||||
</div>
|
||||
|
||||
{/* Grid Card */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{contentImage.length === 0 ? (
|
||||
<div className="col-span-3 text-center text-muted-foreground">
|
||||
Tidak ada artikel tersimpan.
|
||||
{bookmarks.map((bookmark) => (
|
||||
<div key={bookmark.id} className="rounded-xl shadow-md overflow-hidden bg-white hover:shadow-lg transition-shadow">
|
||||
<div className="w-full h-[204px] relative">
|
||||
<Link href={getLink(bookmark)}>
|
||||
<Image
|
||||
src={bookmark.article?.thumbnailUrl || "/placeholder.png"}
|
||||
alt={bookmark.article?.title || "No Title"}
|
||||
fill
|
||||
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</Link>
|
||||
</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">
|
||||
{getContentTypeLabel(bookmark.article?.typeId || 0)}
|
||||
</span>
|
||||
{/* <span className="text-xs font-medium text-[#b3882e]">
|
||||
Bookmark
|
||||
</span> */}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-1">
|
||||
Disimpan: {formatTanggal(bookmark.createdAt)}
|
||||
</p>
|
||||
<Link href={getLink(bookmark)}>
|
||||
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
|
||||
{bookmark.article?.title}
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
<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-green-600" />
|
||||
<ThumbsDown className="w-4 h-4 cursor-pointer hover:text-red-600" />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleDeleteBookmark(bookmark.id, bookmark.article?.id || 0, bookmark.article?.title || "")}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded px-3 text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
contentImage.map((item: any, idx: number) => {
|
||||
const article = item.article || item; // jaga-jaga kalau API return nested
|
||||
return (
|
||||
<PublicationKlFilter
|
||||
key={idx}
|
||||
id={article.id || item.articleId}
|
||||
image={
|
||||
article.thumbnailUrl ||
|
||||
article.mediaUpload?.smallThumbnailLink ||
|
||||
"/contributor.png"
|
||||
}
|
||||
label={article.typeName || article.mediaType?.name || "Artikel"}
|
||||
category={
|
||||
article.categories?.map((c: any) => c.title).join(", ") ||
|
||||
article.tags ||
|
||||
"-"
|
||||
}
|
||||
date={new Date(article.createdAt).toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
})}
|
||||
title={article.title}
|
||||
description={article.description || ""}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,11 @@ export const useAuth = (): AuthContextType => {
|
|||
// Reset rate limiter on successful login
|
||||
loginRateLimiter.resetAttempts(credentials.username);
|
||||
|
||||
router.push("/admin/dashboard");
|
||||
if (profile?.userRoleId === 4 || profile?.userRoleId === 5) {
|
||||
router.push("/");
|
||||
} else {
|
||||
router.push("/admin/dashboard");
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Login failed";
|
||||
setState((prev) => ({
|
||||
|
|
|
|||
|
|
@ -418,3 +418,63 @@ export async function deleteBookmark(id: string | number) {
|
|||
const url = `/bookmarks/${id}`;
|
||||
return await httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
export async function toggleBookmarkById(articleId: string | number) {
|
||||
const url = `/bookmarks/toggle/${articleId}`;
|
||||
return await httpPostInterceptor(url);
|
||||
}
|
||||
|
||||
// Bookmarks API
|
||||
export interface BookmarkItem {
|
||||
id: number;
|
||||
userId: number;
|
||||
articleId: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
article: {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
htmlDescription: string;
|
||||
categoryId: number;
|
||||
typeId: number;
|
||||
tags: string;
|
||||
thumbnailUrl: string;
|
||||
pageUrl: string | null;
|
||||
createdById: number;
|
||||
shareCount: number;
|
||||
viewCount: number;
|
||||
commentCount: number;
|
||||
statusId: number;
|
||||
isBanner: boolean;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookmarksResponse {
|
||||
success: boolean;
|
||||
code: number;
|
||||
messages: string[];
|
||||
data: BookmarkItem[];
|
||||
meta: {
|
||||
limit: number;
|
||||
page: number;
|
||||
nextPage: number;
|
||||
previousPage: number;
|
||||
count: number;
|
||||
totalPage: number;
|
||||
sort: string;
|
||||
sortBy: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getBookmarks(page = 1, limit = 10) {
|
||||
const url = `bookmarks?page=${page}&limit=${limit}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
Loading…
Reference in New Issue