fix: list tenant in register and add api save and bookmark

This commit is contained in:
Sabda Yagra 2025-10-10 23:30:17 +07:00
parent 0029826927
commit e21ff675a9
8 changed files with 748 additions and 391 deletions

View File

@ -1,5 +1,5 @@
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import Cookies from "js-cookie";
// import { close, error, loading } from "@/config/swal";
@ -9,6 +9,13 @@ import withReactContent from "sweetalert2-react-content";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
// import { saveActivity } from "@/service/activity-log";
@ -17,47 +24,25 @@ import { error } from "console";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { createUser, registerTenant } from "@/service/auth";
import { getTenantList } from "@/service/tenant";
interface Tenant {
id: string;
name: string;
}
export default function SignUp() {
const router = useRouter();
const [isVisible, setIsVisible] = useState(false);
const [isVisibleSetup, setIsVisibleSetup] = useState([false, false]);
const [oldEmail, setOldEmail] = useState("");
const [newEmail, setNewEmail] = useState("");
const [passwordSetup, setPasswordSetup] = useState("");
const [confPasswordSetup, setConfPasswordSetup] = useState("");
const toggleVisibility = () => setIsVisible(!isVisible);
const [needOtp, setNeedOtp] = useState(false);
const [isFirstLogin, setFirstLogin] = useState(false);
const [otpValue, setOtpValue] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [accessData, setAccessData] = useState<any>();
const [profile, setProfile] = useState<any>();
const [isValidEmail, setIsValidEmail] = useState(false);
const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal);
const [fullname, setFullname] = useState("");
const [userLevelId, setUserLevelId] = useState("");
const [userRoleId, setUserRoleId] = useState("");
const setValUsername = (e: any) => {
const uname = e.replaceAll(/[^\w.-]/g, "");
setUsername(uname.toLowerCase());
};
const onSubmit = () => {
// Simpan userId dummy ke cookie
Cookies.set("userId", "3");
// (Opsional) Simpan juga data lainnya jika dibutuhkan
// Cookies.set("username", username);
// Redirect ke halaman dashboard atau homepage
router.push("/"); // Ganti dengan path sesuai kebutuhan
};
const [step, setStep] = useState<"login" | "otp">("login");
const [email, setEmail] = useState("");
const [role, setRole] = useState("umum");
@ -82,7 +67,16 @@ export default function SignUp() {
useState("");
const [isLoading, setIsLoading] = useState(false);
const [formErrors, setFormErrors] = useState<any>({});
const [tenantList, setTenantList] = useState<Tenant[]>([]);
useEffect(() => {
getTenant();
}, []);
const getTenant = async () => {
const res = await getTenantList();
setTenantList(res.data.data);
console.log("LLLLLL", res.data.data);
};
const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
@ -178,8 +172,8 @@ export default function SignUp() {
lastEducation: "",
password,
phoneNumber: "",
userLevelId: parseInt(userLevelId),
userRoleId: 3,
userLevelId: 1,
userRoleId: 4,
username: autoUsername,
workType: "",
};
@ -296,7 +290,6 @@ export default function SignUp() {
icon: "success",
confirmButtonText: "OK",
}).then(() => {
// Redirect to login page or dashboard
router.push("/auth");
});
}
@ -416,13 +409,13 @@ export default function SignUp() {
/>
{/* Email */}
<Input
{/* <Input
type="email"
required
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
/> */}
{/* Password */}
<div className="relative">
@ -448,7 +441,7 @@ export default function SignUp() {
</div>
{/* Level dan Role */}
<select
{/* <select
required
className="w-full border border-gray-300 rounded-md p-2 text-sm"
value={userLevelId}
@ -460,9 +453,9 @@ export default function SignUp() {
<option value="3">Satker</option>
<option value="4">Polres</option>
<option value="5">Polsek</option>
</select>
</select> */}
<select
{/* <select
required
className="w-full border border-gray-300 rounded-md p-2 text-sm"
value={userRoleId}
@ -472,7 +465,7 @@ export default function SignUp() {
<option value="1">Admin</option>
<option value="2">Editor</option>
<option value="3">User</option>
</select>
</select> */}
</div>
)}
@ -697,6 +690,34 @@ export default function SignUp() {
</div>
<div>
<Select
value={namaTenant}
onValueChange={(value) => setNamaTenant(value)}
>
<SelectTrigger
className={`w-full ${
formErrors.namaTenant ? "border-red-500" : ""
}`}
>
<SelectValue placeholder="Pilih Tenant" />
</SelectTrigger>
<SelectContent>
{tenantList.map((tenant) => (
<SelectItem key={tenant.id} value={tenant.id}>
{tenant.name}
</SelectItem>
))}
</SelectContent>
</Select>
{formErrors.namaTenant && (
<p className="text-red-500 text-xs mt-1">
{formErrors.namaTenant}
</p>
)}
</div>
{/* <div>
<Input
type="text"
required
@ -712,7 +733,7 @@ export default function SignUp() {
{formErrors.namaTenant}
</p>
)}
</div>
</div> */}
<div className="relative">
<Input

View File

@ -9,15 +9,16 @@ import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { listData, listArticles } from "@/service/landing/landing";
import { getCookiesDecrypt } from "@/lib/utils";
import { toggleBookmark } from "@/service/content";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
export default function Header() {
const [data, setData] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
useEffect(() => {
const fetchData = async () => {
try {
// 🔹 Gunakan API artikel baru
// 🔹 Ambil artikel
const response = await listArticles(
1,
5,
@ -27,6 +28,8 @@ export default function Header() {
"createdAt"
);
let articlesData: any[] = [];
if (response?.error) {
// fallback ke API lama
const fallbackResponse = await listData(
@ -40,13 +43,13 @@ export default function Header() {
"",
""
);
const content = fallbackResponse?.data?.data?.content || [];
setData(content);
return;
articlesData = fallbackResponse?.data?.data?.content || [];
} else {
articlesData = response?.data?.data || [];
}
const articlesData = response?.data?.data || [];
const transformedData = articlesData.map((article: any) => ({
// 🔹 Transform agar seragam
const transformed = articlesData.map((article: any) => ({
id: article.id,
title: article.title,
categoryName:
@ -70,40 +73,79 @@ export default function Header() {
: "",
}));
setData(transformedData);
setData(transformed);
const roleId = Number(getCookiesDecrypt("urie"));
if (roleId && !isNaN(roleId)) {
const saved = localStorage.getItem("bookmarkedIds");
let localSet = new Set<number>();
if (saved) {
localSet = new Set(JSON.parse(saved));
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("Gagal memuat data:", error);
try {
const fallbackResponse = await listData(
"",
"",
"",
5,
0,
"createdAt",
"",
"",
""
);
const content = fallbackResponse?.data?.data?.content || [];
setData(content);
} catch (fallbackError) {
console.error("Fallback API juga gagal:", fallbackError);
}
}
};
fetchData();
}, []);
// Simpan setiap kali state berubah
useEffect(() => {
if (bookmarkedIds.size > 0) {
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(bookmarkedIds))
);
}
}, [bookmarkedIds]);
return (
<section className="max-w-[1350px] mx-auto px-4">
<div className="flex flex-col lg:flex-row gap-6 py-6">
{data.length > 0 && <Card item={data[0]} isBig />}
{data.length > 0 && (
<Card
item={data[0]}
isBig
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
/>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
{data.slice(1, 5).map((item) => (
<Card key={item.id} item={item} />
<Card
key={item.id}
item={item}
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
/>
))}
</div>
</div>
@ -120,14 +162,25 @@ export default function Header() {
);
}
// ========================
// 🔹 CARD COMPONENT
// ========================
function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
function Card({
item,
isBig = false,
isInitiallyBookmarked = false,
onSaved,
}: {
item: any;
isBig?: boolean;
isInitiallyBookmarked?: boolean;
onSaved?: (id: number) => void;
}) {
const router = useRouter();
const MySwal = withReactContent(Swal);
const [isSaving, setIsSaving] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
useEffect(() => {
setIsBookmarked(isInitiallyBookmarked);
}, [isInitiallyBookmarked]);
const getLink = () => {
switch (item?.fileTypeId) {
@ -144,28 +197,23 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
}
};
// ✅ Fungsi simpan bookmark
const handleSave = async () => {
const roleId = Number(getCookiesDecrypt("urie"));
// 🔸 Cek apakah user login (roleId ada & valid)
if (!roleId || roleId === 0 || isNaN(roleId)) {
if (!roleId || isNaN(roleId)) {
MySwal.fire({
icon: "warning",
title: "Login diperlukan",
text: "Silakan login terlebih dahulu untuk menyimpan artikel.",
confirmButtonText: "Login Sekarang",
confirmButtonColor: "#d33",
}).then(() => {
router.push("/auth");
});
}).then(() => router.push("/auth"));
return;
}
try {
setIsSaving(true);
const res = await toggleBookmark(item.id);
console.log("Toggle Bookmark Response:", res);
if (res?.error) {
MySwal.fire({
@ -176,6 +224,17 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
});
} else {
setIsBookmarked(true);
onSaved?.(item.id);
// 🔹 Simpan ke localStorage
const saved = localStorage.getItem("bookmarkedIds");
const newSet = new Set<number>(saved ? JSON.parse(saved) : []);
newSet.add(Number(item.id));
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(newSet))
);
MySwal.fire({
icon: "success",
title: "Berhasil",
@ -265,11 +324,10 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
/>
</div>
{/* ✅ Tombol Save/Disable */}
<button
onClick={handleSave}
disabled={isSaving || isBookmarked}
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 cursor-pointer ${
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
isBookmarked
? "bg-gray-400 text-white cursor-not-allowed"
: "bg-[#F60100] text-white hover:bg-[#c90000]"
@ -283,220 +341,3 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
</div>
);
}
// "use client";
// import Image from "next/image";
// import Link from "next/link";
// import { ThumbsUp, ThumbsDown } from "lucide-react";
// import { useEffect, useState } from "react";
// import {
// listData,
// listArticles,
// } from "@/service/landing/landing";
// export default function Header() {
// const [data, setData] = useState<any[]>([]);
// useEffect(() => {
// const fetchData = async () => {
// try {
// // Use new Articles API
// const response = await listArticles(
// 1,
// 5,
// undefined,
// undefined,
// undefined,
// "createdAt"
// );
// console.log("Articles API response:", response);
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// const fallbackResponse = await listData(
// "",
// "",
// "",
// 5,
// 0,
// "createdAt",
// "",
// "",
// ""
// );
// const content = fallbackResponse?.data?.data?.content || [];
// setData(content);
// return;
// }
// // Handle new API response structure
// const articlesData = response?.data?.data || [];
// console.log("Articles data:", articlesData);
// // Transform articles data to match old structure for backward compatibility
// const transformedData = articlesData.map((article: any) => ({
// id: article.id,
// title: article.title,
// categoryName:
// article.categoryName ||
// (article.categories && article.categories[0]?.title) ||
// "",
// createdAt: article.createdAt,
// smallThumbnailLink: article.thumbnailUrl,
// fileTypeId: article.typeId,
// label:
// article.typeId === 1
// ? "Image"
// : article.typeId === 2
// ? "Video"
// : article.typeId === 3
// ? "Text"
// : article.typeId === 4
// ? "Audio"
// : "",
// ...article,
// }));
// setData(transformedData);
// } catch (error) {
// console.error("Gagal memuat data:", error);
// // Try fallback to old API if new API fails
// try {
// const fallbackResponse = await listData(
// "",
// "",
// "",
// 5,
// 0,
// "createdAt",
// "",
// "",
// ""
// );
// const content = fallbackResponse?.data?.data?.content || [];
// setData(content);
// } catch (fallbackError) {
// console.error("Fallback API also failed:", fallbackError);
// }
// }
// };
// fetchData();
// }, []);
// return (
// <section className="max-w-[1350px] mx-auto px-4">
// <div className="flex flex-col lg:flex-row gap-6 py-6">
// {data.length > 0 && <Card item={data[0]} isBig />}
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
// {data.slice(1, 5).map((item) => (
// <Card key={item.id} item={item} />
// ))}
// </div>
// </div>
// <div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
// <Image
// src={"/PPS.png"}
// alt={"pps"}
// fill
// className="object-cover rounded-xl"
// />
// </div>
// </section>
// );
// }
// function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
// const getLink = () => {
// switch (item?.fileTypeId) {
// 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 "#"; // fallback kalau type tidak dikenali
// }
// };
// return (
// <div>
// <div
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
// isBig
// ? "w-full lg:max-w-[670px] lg:min-h-[680px]"
// : "w-full h-[350px] md:h-[330px]"
// }`}
// >
// <div
// className={`relative ${
// isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
// } w-full`}
// >
// <Link href={getLink()}>
// {" "}
// <Image
// src={item.smallThumbnailLink || "/contributor.png"}
// alt={item.title}
// fill
// className="object-cover"
// />
// </Link>
// </div>
// <div className="p-4 space-y-2">
// <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">
// {item.tenantName}
// </span>
// <span className="text-orange-600">
// {" "}
// {item.categories?.map((cat: any) => cat.title).join(", ")}
// </span>
// </div>
// <div className="text-xs text-gray-500">
// {new Date(item.createdAt)
// .toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// hour12: false,
// timeZone: "Asia/Jakarta",
// })
// .replace(".", ":")}{" "}
// WIB
// </div>
// <Link href={getLink()}>
// {" "}
// <h3 className="text-sm font-semibold leading-snug line-clamp-2">
// {item.title}
// </h3>
// </Link>
// <div className="flex justify-between items-center pt-2">
// <div className="flex gap-2 text-gray-500">
// <ThumbsUp
// size={18}
// className="cursor-pointer hover:text-[#F60100]"
// />
// <ThumbsDown
// size={18}
// className="cursor-pointer hover:text-red-600"
// />
// </div>
// <button className="text-sm bg-[#F60100] text-white px-3 py-1 rounded-md hover:bg-[#F60100]">
// Save
// </button>
// </div>
// </div>
// </div>
// </div>
// );
// }

View File

@ -7,10 +7,14 @@ 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) {
@ -33,7 +37,9 @@ function formatTanggal(dateString: string) {
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);
@ -42,22 +48,21 @@ export default function MediaUpdate() {
async function fetchData(section: "latest" | "popular") {
try {
setLoading(true);
// Use new Articles API
// 🔹 Ambil data artikel
const response = await listArticles(
1,
20,
1, // typeId for images
undefined,
undefined,
1,
20,
1, // typeId = image
undefined,
undefined,
section === "latest" ? "createdAt" : "viewCount"
);
console.log("Media Update Articles API response:", response);
let articlesData: any[] = [];
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
console.error("Articles API failed, fallback ke old API");
const fallbackRes = await listData(
"1",
"",
@ -69,49 +74,132 @@ export default function MediaUpdate() {
"",
""
);
setDataToRender(fallbackRes?.data?.data?.content || []);
return;
articlesData = fallbackRes?.data?.data?.content || [];
} else {
articlesData = response?.data?.data || [];
}
// Handle new API response structure
const articlesData = response?.data?.data || [];
// Transform articles data to match old structure for backward compatibility
// 🔹 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",
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
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);
// Try fallback to old API if new API fails
try {
const fallbackRes = await listData(
"1",
"",
"",
20,
0,
section === "latest" ? "createdAt" : "clickCount",
"",
"",
""
);
setDataToRender(fallbackRes?.data?.data?.content || []);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} 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">
@ -188,11 +276,17 @@ export default function MediaUpdate() {
<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="text-white bg-blue-600 rounded px-4"
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"
}`}
>
Save
{bookmarkedIds.has(Number(item.id)) ? "Saved" : "Save"}
</Button>
</div>
</div>
@ -223,3 +317,229 @@ export default function MediaUpdate() {
</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 { Swiper, SwiperSlide } from "swiper/react";
// import "swiper/css";
// import "swiper/css/navigation";
// import { Navigation } from "swiper/modules";
// // 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 [loading, setLoading] = useState(true);
// useEffect(() => {
// fetchData(tab);
// }, [tab]);
// async function fetchData(section: "latest" | "popular") {
// try {
// setLoading(true);
// // Use new Articles API
// const response = await listArticles(
// 1,
// 20,
// 1, // typeId for images
// undefined,
// undefined,
// section === "latest" ? "createdAt" : "viewCount"
// );
// console.log("Media Update Articles API response:", response);
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// const fallbackRes = await listData(
// "1",
// "",
// "",
// 20,
// 0,
// section === "latest" ? "createdAt" : "clickCount",
// "",
// "",
// ""
// );
// setDataToRender(fallbackRes?.data?.data?.content || []);
// return;
// }
// // Handle new API response structure
// const articlesData = response?.data?.data || [];
// // Transform articles data to match old structure for backward compatibility
// 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);
// } catch (err) {
// console.error("Gagal memuat data:", err);
// // Try fallback to old API if new API fails
// try {
// const fallbackRes = await listData(
// "1",
// "",
// "",
// 20,
// 0,
// section === "latest" ? "createdAt" : "clickCount",
// "",
// "",
// ""
// );
// setDataToRender(fallbackRes?.data?.data?.content || []);
// } catch (fallbackError) {
// console.error("Fallback API also failed:", fallbackError);
// }
// } finally {
// setLoading(false);
// }
// }
// 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
// variant="default"
// size="sm"
// className="text-white bg-blue-600 rounded px-4"
// >
// 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>
// );
// }

View File

@ -3,7 +3,8 @@
import { useEffect, useState } from "react";
import PublicationKlFilter from "./publication-filter";
import { Button } from "@/components/ui/button";
import { mediaWishlist } from "@/service/landing/landing";
import { getCookiesDecrypt } from "@/lib/utils";
import { getBookmarksByUserId, getBookmarksForUser } from "@/service/landing/landing";
const itemsPerPage = 9;
@ -22,43 +23,37 @@ export default function ForYouCardGrid({
selectedCategory,
title,
refresh,
isInstitute = false,
instituteId = "",
}: PublicationCardGridProps) {
const [currentPage, setCurrentPage] = useState(0);
const [contentImage, setContentImage] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [contentImage, setContentImage] = useState<any[]>([]);
const [totalPages, setTotalPages] = useState(1);
const [categoryFilter] = useState([]);
const [formatFilter] = useState([]);
const [sortBy] = useState();
const [categoryFilter] = useState<string[]>([]);
const [formatFilter] = useState<string[]>([]);
const [sortBy] = useState<string>("createdAt");
useEffect(() => {
getDataImage();
getDataBookmark();
}, [currentPage, selectedCategory, title, refresh]);
async function getDataImage() {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: selectedCategory || "";
async function getDataBookmark() {
console.log("📡 Fetching bookmark list...");
const name = title ?? "";
const format = formatFilter?.join(",") ?? "";
try {
const response = await getBookmarksForUser(1, 12, "desc", "createdAt");
console.log("✅ Bookmark response:", response);
const response = await mediaWishlist(
"1",
isInstitute ? instituteId : "",
name,
filter,
"12",
currentPage,
sortBy,
format
);
const bookmarks =
response?.data?.data?.content || response?.data?.data || [];
const totalPage = response?.data?.data?.totalPages || 1;
setTotalPages(response?.data?.data?.totalPages || 1);
setContentImage(response?.data?.data?.content || []);
setContentImage(bookmarks);
setTotalPages(totalPage);
} catch (error) {
console.error("❌ Gagal memuat bookmark:", error);
setContentImage([]);
}
}
const goToPage = (page: number) => {
@ -71,31 +66,39 @@ export default function ForYouCardGrid({
<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 konten ditemukan.
Tidak ada artikel tersimpan.
</div>
) : (
contentImage.map((item: any, idx: number) => (
<PublicationKlFilter
key={item.id}
id={item.id} // ✅ tambahkan ini
image={item.mediaUpload?.smallThumbnailLink}
label={item.mediaType?.name ?? "UNKNOWN"}
category={item.tags ?? "-"}
date={new Date(item?.mediaUpload?.createdAt).toLocaleString(
"id-ID",
{
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={item?.mediaUpload?.title}
description={""} // Optional: Tambahkan jika tersedia dari API
/>
))
})}
title={article.title}
description={article.description || ""}
/>
);
})
)}
</div>
@ -115,3 +118,121 @@ export default function ForYouCardGrid({
</div>
);
}
// "use client";
// import { useEffect, useState } from "react";
// import PublicationKlFilter from "./publication-filter";
// import { Button } from "@/components/ui/button";
// import { mediaWishlist } from "@/service/landing/landing";
// const itemsPerPage = 9;
// type PublicationCardGridProps = {
// selectedCategory: string;
// title?: string;
// refresh?: boolean;
// categoryFilter?: string[];
// formatFilter?: string[];
// isInstitute?: boolean;
// instituteId?: string;
// sortBy?: string;
// };
// export default function ForYouCardGrid({
// selectedCategory,
// title,
// refresh,
// isInstitute = false,
// instituteId = "",
// }: PublicationCardGridProps) {
// const [currentPage, setCurrentPage] = useState(0);
// const [contentImage, setContentImage] = useState([]);
// const [totalPages, setTotalPages] = useState(1);
// const [categoryFilter] = useState([]);
// const [formatFilter] = useState([]);
// const [sortBy] = useState();
// useEffect(() => {
// getDataImage();
// }, [currentPage, selectedCategory, title, refresh]);
// async function getDataImage() {
// const filter =
// categoryFilter?.length > 0
// ? categoryFilter?.sort().join(",")
// : selectedCategory || "";
// const name = title ?? "";
// const format = formatFilter?.join(",") ?? "";
// const response = await mediaWishlist(
// "1",
// isInstitute ? instituteId : "",
// name,
// filter,
// "12",
// currentPage,
// sortBy,
// format
// );
// setTotalPages(response?.data?.data?.totalPages || 1);
// setContentImage(response?.data?.data?.content || []);
// }
// const goToPage = (page: number) => {
// setCurrentPage(page);
// };
// return (
// <div className="space-y-6">
// {/* 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 konten ditemukan.
// </div>
// ) : (
// contentImage.map((item: any, idx: number) => (
// <PublicationKlFilter
// key={item.id}
// id={item.id} // ✅ tambahkan ini
// image={item.mediaUpload?.smallThumbnailLink}
// label={item.mediaType?.name ?? "UNKNOWN"}
// category={item.tags ?? "-"}
// date={new Date(item?.mediaUpload?.createdAt).toLocaleString(
// "id-ID",
// {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// timeZoneName: "short",
// }
// )}
// title={item?.mediaUpload?.title}
// description={""} // Optional: Tambahkan jika tersedia dari API
// />
// ))
// )}
// </div>
// {/* Pagination */}
// <div className="flex justify-center gap-2">
// {Array.from({ length: totalPages }, (_, i) => (
// <Button
// key={i}
// variant={currentPage === i + 1 ? "default" : "outline"}
// size="sm"
// onClick={() => goToPage(i + 1)}
// >
// {i + 1}
// </Button>
// ))}
// </div>
// </div>
// );
// }

View File

@ -214,3 +214,5 @@ export async function registerTenant(data: any) {
const url = "clients/with-user";
return httpPost(url, data);
}

View File

@ -219,7 +219,10 @@ export async function uploadThumbnail(id: any, data: any) {
}
// New Articles API - Upload Article Files
export async function uploadArticleFiles(articleId: string | number, files: FormData) {
export async function uploadArticleFiles(
articleId: string | number,
files: FormData
) {
const url = `article-files/${articleId}`;
const headers = {
"Content-Type": "multipart/form-data",
@ -228,7 +231,10 @@ export async function uploadArticleFiles(articleId: string | number, files: Form
}
// New Articles API - Upload Article Thumbnail
export async function uploadArticleThumbnail(articleId: string | number, thumbnail: FormData) {
export async function uploadArticleThumbnail(
articleId: string | number,
thumbnail: FormData
) {
const url = `articles/thumbnail/${articleId}`;
const headers = {
"Content-Type": "multipart/form-data",
@ -237,7 +243,10 @@ export async function uploadArticleThumbnail(articleId: string | number, thumbna
}
// New Articles API - Get Article Categories
export async function listArticleCategories(page: number = 1, limit: number = 100) {
export async function listArticleCategories(
page: number = 1,
limit: number = 100
) {
const url = `article-categories?page=${page}&limit=${limit}`;
return httpGetInterceptor(url);
}
@ -315,7 +324,7 @@ export async function listArticles(
endDate?: string
) {
let url = `articles?page=${page}&totalPage=${totalPage}`;
// Add optional query parameters based on available filters
if (title) url += `&title=${encodeURIComponent(title)}`;
if (description) url += `&description=${encodeURIComponent(description)}`;
@ -330,7 +339,7 @@ export async function listArticles(
if (isDraft !== undefined) url += `&isDraft=${isDraft}`;
if (startDate) url += `&startDate=${startDate}`;
if (endDate) url += `&endDate=${endDate}`;
return await httpGetInterceptor(url);
}
@ -373,8 +382,9 @@ export async function listDataTeksNew(
) {
// Convert old parameters to new API format
const categoryId = categoryFilter ? Number(categoryFilter) : undefined;
const statusId = statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined;
const statusId =
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined;
return await listArticles(
page + 1, // API expects 1-based page
Number(size),
@ -397,4 +407,14 @@ export async function listDataTeksNew(
export async function toggleBookmark(articleId: string | number) {
const url = `/bookmarks/toggle/${articleId}`;
return httpPostInterceptor(url);
}
export async function getBookmarkSummaryForUser() {
const url = "/bookmarks/summary";
return await httpGetInterceptor(url);
}
export async function deleteBookmark(id: string | number) {
const url = `/bookmarks/${id}`;
return await httpDeleteInterceptor(url);
}

View File

@ -177,12 +177,12 @@ export async function listArticles(
sortBy = "createdAt"
) {
let url = `articles?page=${page}&totalPage=${totalPage}&isPublish=true`;
if (typeId !== undefined) url += `&typeId=${typeId}`;
if (search) url += `&title=${encodeURIComponent(search)}`;
if (categoryId) url += `&categoryId=${categoryId}`;
// if (sortBy) url += `&sortBy=${sortBy}`;
return await httpGetInterceptor(url);
}
@ -241,6 +241,34 @@ export async function mediaWishlist(
);
}
export async function getBookmarksByUserId(
userId: number,
articleId = "",
count = "",
limit = 10,
nextPage = "",
page = 1,
previousPage = "",
sort = "desc",
sortBy = "createdAt",
totalPage = 10
) {
return await httpGetInterceptor(
`bookmarks/user?userId=${userId}&articleId=${articleId}&count=${count}&limit=${limit}&nextPage=${nextPage}&page=${page}&previousPage=${previousPage}&sort=${sort}&sortBy=${sortBy}&totalPage=${totalPage}`
);
}
export async function getBookmarksForUser(
page = 1,
limit = 10,
sort = "desc",
sortBy = "createdAt",
totalPage = 10
) {
const url = `bookmarks/user?page=${page}&limit=${limit}&sort=${sort}&sortBy=${sortBy}&totalPage=${totalPage}`;
return await httpGetInterceptor(url);
}
export async function checkWishlistStatus(mediaId: any) {
return await httpGetInterceptor(`/media/wishlist/status?mediaId=${mediaId}`);
}

View File

@ -14,5 +14,9 @@ export async function getUserLevelDetail(id: number) {
const url = `user-levels/${id}`;
return httpGetInterceptor(url);
}
export async function getTenantList() {
const url = `clients?limit=1000`;
return httpGetInterceptor(url);
}