fix: add landing page tenant and add api bookmark in landing page

This commit is contained in:
Sabda Yagra 2025-10-09 23:43:53 +07:00
parent d59857b559
commit 0029826927
17 changed files with 427 additions and 49 deletions

View File

@ -59,7 +59,7 @@ function TenantSettingsContent() {
setIsLoading(true);
try {
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
getApprovalWorkflowComprehensiveDetails(), // Using workflow ID 4 as per API example
getApprovalWorkflowComprehensiveDetails(),
getUserLevels(),
]);

View File

@ -0,0 +1,29 @@
"use client";
import LayoutProvider from "@/providers/layout.provider";
import LayoutContentProvider from "@/providers/content.provider";
import DashCodeSidebar from "@/components/partials/sidebar";
import DashCodeFooter from "@/components/partials/footer";
import ThemeCustomize from "@/components/partials/customizer";
import DashCodeHeader from "@/components/partials/header";
import { redirect } from "@/components/navigation";
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
import { useParams } from "next/navigation";
const layout = ({ children }: { children: React.ReactNode }) => {
const params = useParams();
const poldaName: any = params?.polda_name;
return (
// children
// ) : (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

@ -0,0 +1,19 @@
import Category from "@/components/landing-page/category";
import Footer from "@/components/landing-page/footer";
import Header from "@/components/landing-page/header";
import MediaUpdate from "@/components/landing-page/media-update";
import React from "react";
const page = () => {
return (
<div>
<div className="flex-1">
<Header />
</div>
<MediaUpdate />
<Category />
</div>
);
};
export default page;

View File

@ -0,0 +1,24 @@
"use client";
import Image from "next/image";
import { usePathname } from "next/navigation";
export const DynamicLogoTenant = () => {
const pathname = usePathname();
const tenant = pathname?.split("/")[3];
return (
<div className="p-2">
{pathname?.includes("/tenant") && (
<Image
priority={true}
src={`/logo/${tenant}.png`}
alt="Logo"
width={1920}
height={1080}
className="object-contain h-[50px] w-[50px] ml-10"
/>
)}
</div>
);
};

View File

@ -4,15 +4,26 @@ import { Instagram, ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import { useRef } from "react";
// const logos = [
// { src: "/mabes.png", href: "/in/public/publication/kl" },
// { src: "/saw.png", href: "/" },
// { src: "/mpr.png", href: "/" },
// { src: "/dpr.png", href: "/" },
// { src: "/ma.png", href: "/" },
// { src: "/badan.png", href: "/" },
// { src: "/kpk.png", href: "/" },
// { src: "/pln.png", href: "/" },
// ];
const logos = [
{ src: "/mabes.png", href: "/public/publication/kl" },
{ src: "/saw.png", href: "/" },
{ src: "/mpr.png", href: "/" },
{ src: "/dpr.png", href: "/" },
{ src: "/ma.png", href: "/" },
{ src: "/badan.png", href: "/" },
{ src: "/kpk.png", href: "/" },
{ src: "/pln.png", href: "/" },
{ src: "/mabes.png", slug: "mabes" },
{ src: "/saw.png", slug: "kejaksaan-agung" },
{ src: "/mpr.png", slug: "mpr-ri" },
{ src: "/dpr.png", slug: "dpr-ri" },
{ src: "/ma.png", slug: "mahkamah-agung" },
{ src: "/badan.png", slug: "kemenkeu" },
{ src: "/kpk.png", slug: "kpk" },
{ src: "/pln.png", slug: "pupr" },
];
export default function Footer() {
@ -51,7 +62,7 @@ export default function Footer() {
{logos.map((logo, idx) => (
<a
key={idx}
href={logo.href}
href={`/in/tenant/${logo.slug}`}
target="_blank"
rel="noopener noreferrer"
>

View File

@ -1,24 +1,23 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { useEffect, useState } from "react";
import {
getListContent,
listData,
listArticles,
listStaticBanner,
} from "@/service/landing/landing";
import { data } from "framer-motion/client";
import { useRouter } from "next/navigation";
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";
export default function Header() {
const [data, setData] = useState<any[]>([]);
const [selectedTab, setSelectedTab] = useState("image");
useEffect(() => {
const fetchData = async () => {
try {
// Use new Articles API
// 🔹 Gunakan API artikel baru
const response = await listArticles(
1,
5,
@ -27,11 +26,9 @@ export default function Header() {
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
// fallback ke API lama
const fallbackResponse = await listData(
"",
"",
@ -48,11 +45,7 @@ export default function Header() {
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,
@ -63,6 +56,8 @@ export default function Header() {
createdAt: article.createdAt,
smallThumbnailLink: article.thumbnailUrl,
fileTypeId: article.typeId,
tenantName: article.tenantName,
categories: article.categories,
label:
article.typeId === 1
? "Image"
@ -73,13 +68,11 @@ export default function Header() {
: 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(
"",
@ -95,7 +88,7 @@ export default function Header() {
const content = fallbackResponse?.data?.data?.content || [];
setData(content);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
console.error("Fallback API juga gagal:", fallbackError);
}
}
};
@ -127,7 +120,15 @@ export default function Header() {
);
}
// ========================
// 🔹 CARD COMPONENT
// ========================
function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
const router = useRouter();
const MySwal = withReactContent(Swal);
const [isSaving, setIsSaving] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(false);
const getLink = () => {
switch (item?.fileTypeId) {
case 1:
@ -139,11 +140,66 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
case 4:
return `/content/audio/detail/${item?.id}`;
default:
return "#"; // fallback kalau type tidak dikenali
return "#";
}
};
// ✅ Fungsi simpan bookmark
const handleSave = async () => {
const roleId = Number(getCookiesDecrypt("urie"));
// 🔸 Cek apakah user login (roleId ada & valid)
if (!roleId || roleId === 0 || 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");
});
return;
}
try {
setIsSaving(true);
const res = await toggleBookmark(item.id);
console.log("Toggle Bookmark Response:", res);
if (res?.error) {
MySwal.fire({
icon: "error",
title: "Gagal",
text: "Gagal menyimpan artikel.",
confirmButtonColor: "#d33",
});
} else {
setIsBookmarked(true);
MySwal.fire({
icon: "success",
title: "Berhasil",
text: "Artikel berhasil disimpan ke bookmark.",
confirmButtonColor: "#3085d6",
timer: 1500,
showConfirmButton: false,
});
}
} catch (err) {
console.error("Toggle bookmark error:", err);
MySwal.fire({
icon: "error",
title: "Kesalahan",
text: "Terjadi kesalahan saat menyimpan artikel.",
confirmButtonColor: "#d33",
});
} finally {
setIsSaving(false);
}
};
return (
<Link href={getLink()}>
<div>
<div
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
isBig
@ -156,24 +212,26 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
} w-full`}
>
<Image
src={item.smallThumbnailLink || "/contributor.png"}
alt={item.title}
fill
className="object-cover"
/>
<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.categoryName}
{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", {
@ -188,27 +246,257 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
.replace(".", ":")}{" "}
WIB
</div>
<h3 className="text-sm font-semibold leading-snug line-clamp-2">
{item.title}
</h3>
<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-blue-600"
className="cursor-pointer hover:text-[#F60100]"
/>
<ThumbsDown
size={18}
className="cursor-pointer hover:text-red-600"
/>
</div>
<button className="text-sm bg-blue-600 text-white px-3 py-1 rounded-md hover:bg-blue-700">
Save
{/* ✅ Tombol Save/Disable */}
<button
onClick={handleSave}
disabled={isSaving || isBookmarked}
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 cursor-pointer ${
isBookmarked
? "bg-gray-400 text-white cursor-not-allowed"
: "bg-[#F60100] text-white hover:bg-[#c90000]"
}`}
>
{isSaving ? "Saving..." : isBookmarked ? "Saved" : "Save"}
</button>
</div>
</div>
</div>
</Link>
</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

@ -3,13 +3,14 @@
import { useState, useEffect } from "react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { Menu, ChevronDown } from "lucide-react";
import { Button } from "../ui/button";
import Cookies from "js-cookie";
import { Card } from "../ui/card";
import { Input } from "../ui/input";
import { usePathname } from "next/navigation";
import { Link } from "@/i18n/routing";
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
const NAV_ITEMS = [
{ label: "Beranda", href: "/" },
@ -21,7 +22,7 @@ const NAV_ITEMS = [
const PUBLIKASI_SUBMENU = [
{ label: "K/L", href: "/public/publication/kl" },
{ label: "BUMN", href: "/public/publication/bumn" },
{ label: "BUMN", href: "/in/public/publication/bumn" },
{ label: "Pemerintah Daerah", href: "/public/publication/pemerintah-daerah" },
];
@ -170,6 +171,7 @@ export default function Navbar() {
className="w-6 h-6 cursor-pointer"
onClick={() => setIsSidebarOpen(true)}
/>
<DynamicLogoTenant />
</div>
<nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium">

View File

@ -15,7 +15,7 @@ export default function PublicationKlLayout() {
useEffect(() => {
const roleId = getCookiesDecrypt("urie") ?? "";
const allowedRoles = ["6", "7"];
const allowedRoles = ["6", "7", "2", "3"];
if (!allowedRoles.includes(roleId)) {
router.push("/auth");

BIN
public/logo/dpr-ri.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
public/logo/kemenkeu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/logo/kpk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/logo/mabes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/logo/mpr-ri.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/logo/pupr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@ -392,4 +392,9 @@ export async function listDataTeksNew(
startDate,
endDate
);
}
export async function toggleBookmark(articleId: string | number) {
const url = `/bookmarks/toggle/${articleId}`;
return httpPostInterceptor(url);
}