Compare commits

...

10 Commits

Author SHA1 Message Date
Anang Yusman d011f5bb02 update domain 2026-02-16 14:05:02 +08:00
Anang Yusman c201123581 update ckeditor 2026-02-10 15:27:04 +08:00
Anang Yusman b0dc31a3f0 update landing 2026-02-05 11:25:52 +08:00
Anang Yusman 67536a6d31 update 2026-01-06 11:26:09 +08:00
Anang Yusman 91f97420b7 update 2026-01-02 14:55:38 +08:00
Anang Yusman 7e338b5625 update 2025-12-23 16:51:51 +08:00
Anang Yusman 2ef6287447 update 2025-12-18 11:38:38 +08:00
Anang Yusman b60347213a update 2025-12-15 11:11:40 +08:00
Anang Yusman b62b31c1d0 update ci 2025-12-11 14:11:50 +08:00
Anang Yusman 1787bc51f6 update 2025-11-02 18:38:32 +08:00
27 changed files with 1467 additions and 632 deletions

View File

@ -7,15 +7,16 @@ build-dev:
when: on_success
only:
- main
image: docker:stable
image:
name: docker:25.0.3-cli
services:
- name: docker:dind
command: ["--insecure-registry=103.82.242.92:8900"]
- name: docker:25.0.3-dind
command: ["--insecure-registry=38.47.185.86:8900"]
script:
- docker logout
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker build -t 103.82.242.92:8900/medols/web-kabar-harapan:dev .
- docker push 103.82.242.92:8900/medols/web-kabar-harapan:dev
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
- docker build -t 38.47.185.86:8900/medols/web-kabar-harapan:dev .
- docker push 38.47.185.86:8900/medols/web-kabar-harapan:dev
auto-deploy:
stage: deploy
@ -26,4 +27,4 @@ auto-deploy:
services:
- docker:dind
script:
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-kabar-harapan/build?token=autodeploymedols
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-kabar-harapan/build?token=autodeploymedols

View File

@ -0,0 +1,19 @@
import DetailContent from "@/components/details/details-content";
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
import Image from "next/image";
export default function Slug() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar />
<div className="flex-1">
<DetailContent />
</div>
<Footer />
</div>
</div>
);
}

View File

@ -1,20 +1,25 @@
import Footer from "@/components/landing-page/footer";
import Header from "@/components/landing-page/headers";
import Header from "@/components/landing-page/header";
import Navbar from "@/components/landing-page/navbar";
import BreakingNews from "@/components/landing-page/breaking-news";
import Image from "next/image";
import News from "@/components/landing-page/news";
import LatestNews from "@/components/landing-page/latest-news";
import Development from "@/components/landing-page/development";
import OpinionNews from "@/components/landing-page/opinion-news";
import NewsTerkini from "@/components/landing-page/health";
import YouTubeSection from "@/components/landing-page/youtube-selection";
export default function Home() {
return (
<div className="flex min-h-screen flex-col font-[family-name:var(--font-geist-sans)] bg-white">
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
{/* Background fixed tidak ikut scroll */}
<Navbar />
<div className="flex-1 bg-gray-300 mt-10">
<div className="flex-1">
<Header />
<BreakingNews />
<News />
</div>
<LatestNews />
<Development />
<OpinionNews />
<NewsTerkini />
<YouTubeSection />
<Footer />
</div>
);

View File

@ -2,7 +2,11 @@
import Image from "next/image";
import { useEffect, useState } from "react";
import Link from "next/link";
import { getArticleById, getListArticle } from "@/service/article";
import {
getArticleById,
getArticleBySlug,
getListArticle,
} from "@/service/article";
import { close, error, loading } from "@/config/swal";
import { useParams, usePathname } from "next/navigation";
import { Link2, MailIcon } from "lucide-react";
@ -16,6 +20,9 @@ import {
} from "@/service/master-user";
import { saveActivity } from "@/service/activity-log";
import { useForm } from "react-hook-form";
import { Badge } from "../ui/badge";
import path from "path";
import { formatTextToHtmlTag } from "@/utils/global";
type TabKey = "trending" | "comments" | "latest";
@ -25,6 +32,7 @@ type Article = {
description: string;
categoryName: string;
createdAt: string;
slug: string;
createdByName: string;
thumbnailUrl: string;
categories: {
@ -54,6 +62,7 @@ type Advertise = {
export default function DetailContent() {
const params = useParams();
const id = params?.id;
const slug = params?.slug;
const pathname = usePathname();
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
@ -73,7 +82,7 @@ export default function DetailContent() {
const [diseId, setDiseId] = useState(0);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null
null,
);
const [selectedIndex, setSelectedIndex] = useState(0);
@ -296,13 +305,13 @@ export default function DetailContent() {
async function initStateData() {
loading();
const res = await getArticleById(id);
const data = res.data?.data;
const res = await getArticleBySlug(slug);
const data = res?.data?.data;
setThumbnail(data?.thumbnailUrl);
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files);
setArticleDetail(data);
setArticleDetail(data); // <-- Add this
close();
}
@ -314,6 +323,16 @@ export default function DetailContent() {
);
}
function removeImgTags(htmlString?: { __html: string }) {
const parser = new DOMParser();
const doc = parser.parseFromString(String(htmlString?.__html), "text/html");
const images = doc.querySelectorAll("img");
images.forEach((img) => img.remove());
return { __html: doc.body.innerHTML };
}
return (
<>
<div className="bg-white grid grid-cols-1 md:grid-cols-3 gap-6 px-8 py-8">
@ -349,14 +368,13 @@ export default function DetailContent() {
<span></span>
<span>
<span>
{new Date(articleDetail?.publishedAt).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
{new Date(
articleDetail?.publishedAt ?? articleDetail?.createdAt,
).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
</span>
<span></span>
@ -472,9 +490,10 @@ export default function DetailContent() {
<div className="flex-1 overflow-y-auto">
<div className="text-gray-700 leading-relaxed text-justify">
<div
dangerouslySetInnerHTML={{
__html: articleDetail?.htmlDescription || "",
}}
dangerouslySetInnerHTML={removeImgTags(
formatTextToHtmlTag(articleDetail?.htmlDescription),
)}
className="text-sm lg:text-xl lg:leading-8 text-justify space-y-4"
/>
</div>
{/* <Author /> */}
@ -526,9 +545,17 @@ export default function DetailContent() {
</span>
<div className="flex flex-wrap gap-2 mt-1">
{articleDetail?.tags ? (
<span className="bg-gray-100 text-gray-700 text-sm px-2 py-1 rounded">
{articleDetail.tags}
</span>
articleDetail.tags
.split(",") // pisahkan berdasarkan koma
.map((tag: string, index: number) => (
<Badge
key={index}
variant="secondary"
className="text-sm"
>
{tag.trim()}
</Badge>
))
) : (
<span className="text-sm text-gray-500">Tidak ada tag</span>
)}
@ -724,7 +751,7 @@ export default function DetailContent() {
{/* Artikel utama (featured) */}
{articles.length > 0 && (
<div className="relative">
<Link href={`/detail/${articles[0]?.id}`}>
<Link href={`/details/${articles[0]?.slug}`}>
<Image
src={articles[0]?.thumbnailUrl || "/default.jpg"}
alt={articles[0]?.title || "Recommended Article"}
@ -744,7 +771,7 @@ export default function DetailContent() {
day: "2-digit",
month: "long",
year: "numeric",
}
},
)}
</p>
</div>
@ -758,7 +785,7 @@ export default function DetailContent() {
<div key={item.id}>
<Link
className="flex space-x-3"
href={`/detail/${item?.id}`}
href={`/details/${item?.slug}`}
>
<Image
src={item.thumbnailUrl || "/default.jpg"}
@ -779,7 +806,7 @@ export default function DetailContent() {
day: "2-digit",
month: "long",
year: "numeric",
}
},
)}
</p>
</div>

View File

@ -1,7 +1,7 @@
// components/custom-editor.js
import React from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import "@/styles/custom-editor.css";
import Editor from "@/vendor/ckeditor5/build/ckeditor";
function CustomEditor(props) {
@ -47,7 +47,7 @@ function CustomEditor(props) {
padding: 1rem;
}
p {
margin: 0.5em 0;
margin: 0.5em 0 !important;
}
h1, h2, h3, h4, h5, h6 {
margin: 1em 0 0.5em 0;
@ -72,98 +72,6 @@ function CustomEditor(props) {
},
}}
/>
<style jsx>{`
.ckeditor-wrapper {
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.ckeditor-wrapper :global(.ck.ck-editor__main) {
min-height: ${props.height || 400}px;
max-height: ${maxHeight}px;
}
.ckeditor-wrapper :global(.ck.ck-editor__editable) {
min-height: ${(props.height || 400) - 50}px;
max-height: ${maxHeight - 50}px;
overflow-y: auto !important;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
background: #fff !important;
color: #111 !important;
}
/* Dark mode support */
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable) {
background: #111 !important;
color: #f9fafb !important;
}
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h1),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h2),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h3),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h4),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h5),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h6) {
color: #f9fafb !important;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable blockquote) {
background-color: #1f2937 !important;
border-left-color: #374151 !important;
color: #f3f4f6 !important;
}
/* Custom scrollbar styling for webkit browsers */
.ckeditor-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
width: 8px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #f1f5f9;
border-radius: 4px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #cbd5e1;
border-radius: 4px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #94a3b8;
}
/* Dark mode scrollbar */
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #1f2937;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #4b5563;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #6b7280;
}
/* Ensure content doesn't overflow */
.ckeditor-wrapper :global(.ck.ck-editor__editable .ck-content) {
overflow: hidden;
}
`}</style>
</div>
);
}

View File

@ -48,7 +48,8 @@ function ViewEditor(props) {
.ckeditor-view-wrapper {
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
}

View File

@ -9,6 +9,7 @@ type Article = {
description: string;
categoryName: string;
createdAt: string;
slug: string;
createdByName: string;
thumbnailUrl: string;
categories: { title: string }[];
@ -51,7 +52,7 @@ export default function BannerNews() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 p-4">
{/* Artikel utama */}
<div className="lg:col-span-2 relative">
<Link href={`/detail/${mainArticle?.id}`}>
<Link href={`/details/${mainArticle?.slug}`}>
<img
src={mainArticle.thumbnailUrl || mainArticle.files?.[0]?.fileUrl}
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
@ -72,7 +73,7 @@ export default function BannerNews() {
<div className="grid grid-cols-1 gap-4">
{sideArticles.map((article) => (
<div key={article.id} className="relative">
<Link href={`/detail/${article?.id}`}>
<Link href={`/details/${article?.slug}`}>
<img
src={article.thumbnailUrl || article.files?.[0]?.fileUrl}
alt={article.files?.[0]?.file_alt || article.title}

View File

@ -13,6 +13,7 @@ type Article = {
description: string;
categoryName: string;
createdAt: string;
slug: string;
createdByName: string;
thumbnailUrl: string;
categories: { title: string }[];
@ -79,7 +80,7 @@ export default function BreakingNews() {
key={article.id}
className="bg-white border overflow-hidden shadow-sm hover:shadow-md transition"
>
<Link href={`/detail/${article?.id}`}>
<Link href={`/details/${article?.slug}`}>
<div className="relative w-full h-40">
<Image
src={

View File

@ -0,0 +1,136 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { getListArticle } from "@/service/article";
import Link from "next/link";
type Article = {
id: number;
title: string;
description: string;
categoryName: string;
slug: string;
createdAt: string;
publishedAt: string;
createdByName: string;
customCreatorName: string;
thumbnailUrl: string;
categories: { title: string }[];
files: { fileUrl: string; file_alt: string }[];
};
export default function Development() {
const [articles, setArticles] = useState<Article[]>([]);
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
useEffect(() => {
initState();
}, [page]);
async function initState() {
const req = {
limit: "10",
page,
search: "",
categorySlug: "",
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
// Format tanggal ke gaya lokal
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
});
};
// Mapping struktur seperti dummy sebelumnya
const leftMain = articles[0];
const leftList = articles.slice(1, 4);
const centerMain = articles[4];
const centerList = articles.slice(5, 8);
const rightMain = articles[8];
const rightList = articles.slice(9, 12);
return (
<section className="max-w-7xl mx-auto px-4">
<div className="bg-white ">
<div className="mb-4">
<h2 className="text-xl font-black text-[#000]">JAGA NEGERI</h2>
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
</div>
<div className="border-b border-gray-300 mb-5"></div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{articles.slice(0, 6).map((item) => (
<Link
href={`/details/${item.slug}`}
key={item.id}
className="flex gap-4 pb-4 border-b border-gray-200"
>
<div className="relative w-28 h-28 rounded-md overflow-hidden flex-shrink-0">
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
fill
className="object-cover"
/>
</div>
<div className="flex flex-col">
<p className="text-[11px] font-bold text-black">
<span className="border-b-2 border-green-600 pb-[1px]">
{item.categories?.[0]?.title || "Kategori"}
</span>
</p>
<h3 className="font-semibold text-[15px] leading-tight mt-1 line-clamp-2">
{item.title}
</h3>
<p className="text-[11px] text-gray-600 mt-2">
By{" "}
<span className="font-semibold">
{item.customCreatorName}
</span>
</p>
<p className="text-[11px] text-gray-600">
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
</div>
</Link>
))}
</div>
<div className="relative h-[140px] w-full overflow-hidden rounded-none my-5">
<Image
src="/image-kolom.png"
alt="Berita Utama"
fill
className="object-fill"
/>
</div>
</div>
</section>
);
}

View File

@ -1,106 +1,69 @@
"use client";
// components/Footer.tsx
import Image from "next/image";
import { Facebook, Twitter, Instagram, Youtube } from "lucide-react";
import Link from "next/link";
export default function Footer() {
return (
<footer className="bg-[#1a1a1a] text-gray-300 py-12">
<div className="max-w-5xl mx-auto text-center px-4">
<div className="relative inline-block mb-6">
<Image
src="/harapan.png"
alt="Kabar Harapan"
width={977}
height={353}
className="mx-auto"
/>
<footer className="bg-[#ECEFF5] pt-20 pb-10 w-full">
<div className="max-w-screen-xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 ">
{/* Logo */}
<div className="flex items-center justify-end">
<div className="flex justify-center md:justify-end">
<Image
src="/harapan.png"
alt="Logo"
width={230}
height={230}
className="object-contain"
/>
</div>
</div>
<p className="max-w-2xl mx-auto text-sm mb-4">
KabarHarapan.com adalah media online yang berkomitmen untuk
menyebarkan berita yang menginspirasi dan mendorong perubahan positif
di masyarakat. Kami percaya bahwa di balik setiap cerita, ada potensi
untuk memberikan harapan dan memberdayakan individu untuk menciptakan
perbedaan dalam dunia ini.
</p>
{/* Subscribe Box */}
<div className="flex justify-end md:justify-end pl-5">
<div className=" w-full ml-5">
<h2 className="text-2xl font-semibold text-gray-800 leading-snug">
Subscribe us to get <br />
the latest news!
</h2>
<p className="text-sm mb-6">
Contact us:{" "}
<a
href="mailto:contact@kabarharapan.com"
className="text-blue-400 hover:underline"
>
contact@kabarharapan.com
</a>
</p>
<label className="block mt-6 mb-1 text-sm text-gray-600">
Email address:
</label>
<div className="flex justify-center space-x-6">
<a
href="#"
className="bg-blue-600 p-3 rounded hover:opacity-80 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.602 21.026v-7.274H6.818a.545.545 0 0 1-.545-.545V10.33a.545.545 0 0 1 .545-.545h2.773V7a4.547 4.547 0 0 1 4.86-4.989h2.32a.556.556 0 0 1 .557.546v2.436a.557.557 0 0 1-.557.545h-1.45c-1.566 0-1.867.742-1.867 1.833v2.413h3.723a.533.533 0 0 1 .546.603l-.337 2.888a.545.545 0 0 1-.545.476h-3.364v7.274a.96.96 0 0 1-.975.974h-1.937a.96.96 0 0 1-.963-.974"
/>
</svg>
</a>
<a
href="#"
className="bg-sky-400 p-3 rounded hover:opacity-80 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22.213 5.656a8.4 8.4 0 0 1-2.402.658A4.2 4.2 0 0 0 21.649 4c-.82.488-1.719.83-2.655 1.015a4.182 4.182 0 0 0-7.126 3.814a11.87 11.87 0 0 1-8.621-4.37a4.17 4.17 0 0 0-.566 2.103c0 1.45.739 2.731 1.86 3.481a4.2 4.2 0 0 1-1.894-.523v.051a4.185 4.185 0 0 0 3.355 4.102a4.2 4.2 0 0 1-1.89.072A4.185 4.185 0 0 0 8.02 16.65a8.4 8.4 0 0 1-6.192 1.732a11.83 11.83 0 0 0 6.41 1.88c7.694 0 11.9-6.373 11.9-11.9q0-.271-.012-.541a8.5 8.5 0 0 0 2.086-2.164"
/>
</svg>
</a>
<a
href="#"
className="bg-gradient-to-tr from-purple-500 via-pink-500 to-yellow-500 p-3 rounded hover:opacity-80 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12.001 9a3 3 0 1 0 0 6a3 3 0 0 0 0-6m0-2a5 5 0 1 1 0 10a5 5 0 0 1 0-10m6.5-.25a1.25 1.25 0 0 1-2.5 0a1.25 1.25 0 0 1 2.5 0M12.001 4c-2.474 0-2.878.007-4.029.058c-.784.037-1.31.142-1.798.332a2.9 2.9 0 0 0-1.08.703a2.9 2.9 0 0 0-.704 1.08c-.19.49-.295 1.015-.331 1.798C4.007 9.075 4 9.461 4 12c0 2.475.007 2.878.058 4.029c.037.783.142 1.31.331 1.797c.17.435.37.748.702 1.08c.337.336.65.537 1.08.703c.494.191 1.02.297 1.8.333C9.075 19.994 9.461 20 12 20c2.475 0 2.878-.007 4.029-.058c.782-.037 1.308-.142 1.797-.331a2.9 2.9 0 0 0 1.08-.703c.337-.336.538-.649.704-1.08c.19-.492.296-1.018.332-1.8c.052-1.103.058-1.49.058-4.028c0-2.474-.007-2.878-.058-4.029c-.037-.782-.143-1.31-.332-1.798a2.9 2.9 0 0 0-.703-1.08a2.9 2.9 0 0 0-1.08-.704c-.49-.19-1.016-.295-1.798-.331C14.926 4.006 14.54 4 12 4m0-2c2.717 0 3.056.01 4.123.06c1.064.05 1.79.217 2.427.465c.66.254 1.216.598 1.772 1.153a4.9 4.9 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428c.047 1.066.06 1.405.06 4.122s-.01 3.056-.06 4.122s-.218 1.79-.465 2.428a4.9 4.9 0 0 1-1.153 1.772a4.9 4.9 0 0 1-1.772 1.153c-.637.247-1.363.415-2.427.465c-1.067.047-1.406.06-4.123.06s-3.056-.01-4.123-.06c-1.064-.05-1.789-.218-2.427-.465a4.9 4.9 0 0 1-1.772-1.153a4.9 4.9 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.012 15.056 2 14.717 2 12s.01-3.056.06-4.122s.217-1.79.465-2.428a4.9 4.9 0 0 1 1.153-1.772A4.9 4.9 0 0 1 5.45 2.525c.637-.248 1.362-.415 2.427-.465C8.945 2.013 9.284 2 12.001 2"
/>
</svg>
</a>
<a
href="#"
className="bg-red-600 p-3 rounded hover:opacity-80 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m10 15l5.19-3L10 9zm11.56-7.83c.13.47.22 1.1.28 1.9c.07.8.1 1.49.1 2.09L22 12c0 2.19-.16 3.8-.44 4.83c-.25.9-.83 1.48-1.73 1.73c-.47.13-1.33.22-2.65.28c-1.3.07-2.49.1-3.59.1L12 19c-4.19 0-6.8-.16-7.83-.44c-.9-.25-1.48-.83-1.73-1.73c-.13-.47-.22-1.1-.28-1.9c-.07-.8-.1-1.49-.1-2.09L2 12c0-2.19.16-3.8.44-4.83c.25-.9.83-1.48 1.73-1.73c.47-.13 1.33-.22 2.65-.28c1.3-.07 2.49-.1 3.59-.1L12 5c4.19 0 6.8.16 7.83.44c.9.25 1.48.83 1.73 1.73"
/>
</svg>
</a>
<input
type="email"
placeholder="Your email address"
className="w-full border border-gray-300 rounded-md px-4 py-3 outline-none"
/>
<button className="mt-4 bg-green-600 hover:bg-green-500 text-black px-6 py-3 rounded-md font-medium">
SIGN UP
</button>
</div>
</div>
</div>
<div className="flex flex-wrap justify-center gap-8 mt-16 text-gray-600 text-sm">
<a href="#">About Us</a>
<a href="#">Contact</a>
<a href="#">Kode Etik Jurnalistik</a>
<a href="#">Kebijakan Privasi</a>
<a href="#">Disclaimer</a>
<a href="#">Pedoman Media Siber</a>
</div>
<div className="flex justify-center gap-8 mt-8 text-gray-700">
<Facebook className="w-5 h-5 cursor-pointer" />
<Twitter className="w-5 h-5 cursor-pointer" />
<Instagram className="w-5 h-5 cursor-pointer" />
<Youtube className="w-5 h-5 cursor-pointer" />
</div>
<p className="text-start text-gray-500 text-sm mt-8 pl-5">
© 2025 Milenial Bersuara - All Rights Reserved.
</p>
</footer>
);
}

View File

@ -0,0 +1,232 @@
"use client";
import { getListArticle } from "@/service/article";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
type Article = {
id: number;
title: string;
description: string;
categoryName: string;
createdAt: string;
slug: string;
createdByName: string;
publishedAt: string;
customCreatorName: string;
thumbnailUrl: string;
categories: { title: string }[];
files: { fileUrl: string; file_alt: string }[];
};
export default function Header() {
const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => {
const fetchArticles = async () => {
const req = {
limit: "10",
page: 1,
search: "",
categorySlug: "",
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
};
fetchArticles();
}, []);
const flashArticles = articles.slice(0, 8);
const mainArticle = articles[8] || articles[0];
const recentPosts = articles.slice(1, 5);
return (
<section className="max-w-7xl mx-auto bg-white px-4">
{/* FLASH STRIP */}
<div className="flex items-center justify-between mt-6 mb-3">
<div className="flex items-center gap-3">
<h4 className="text-green-600 font-semibold">Flash</h4>
<span className="text-red-500"></span>
</div>
<div className="text-xs text-gray-500 hidden md:block">LOAD MORE </div>
</div>
<div className="overflow-x-auto no-scrollbar py-2">
<div className="flex gap-4">
{flashArticles.map((item) => (
<Link
href={`/details/${item.slug}`}
key={`flash-${item.id}`}
className="min-w-[200px] md:min-w-[220px] bg-gray-800 rounded-lg overflow-hidden relative shadow"
>
<div className="relative w-[200px] md:w-[220px] h-[140px]">
<Image
src={
item.thumbnailUrl ||
item.files?.[0]?.fileUrl ||
"/placeholder.jpg"
}
alt={item.title}
fill
className="object-cover"
/>
</div>
{/* dark overlay with text */}
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent text-white">
<p className="text-xs line-clamp-2">{item.title}</p>
<div className="flex items-center justify-between mt-2 text-[11px] text-gray-300">
<span className="text-yellow-300 bg-black/30 px-1 rounded-sm">
{item.categoryName ||
item.categories?.[0]?.title ||
"Berita"}
</span>
<span></span>
</div>
</div>
{/* play icon */}
<div className="absolute top-3 right-3 w-8 h-8 bg-white/80 rounded-full flex items-center justify-center">
<svg
className="w-4 h-4 text-black"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M8 5v14l11-7z" />
</svg>
</div>
</Link>
))}
</div>
</div>
{/* Main Layout */}
<div className="grid grid-cols-1 md:grid-cols-[2.2fr_1fr] gap-6">
{/* LEFT SIDE MAIN ARTICLE */}
{mainArticle ? (
<div className="relative h-[500px] w-full rounded-xl overflow-hidden shadow-md">
<Link href={`/details/${mainArticle.slug}`}>
<Image
src={
mainArticle.thumbnailUrl ||
mainArticle.files?.[0]?.fileUrl ||
"/placeholder.jpg"
}
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
fill
className="object-cover"
/>
{/* White Card Overlay */}
<div className="absolute bottom-6 left-6 bg-white bg-opacity-95 backdrop-blur-sm p-6 shadow-lg max-w-lg">
<span className="text-[11px] bg-green-700 text-white px-2 py-1 rounded-sm">
{mainArticle.categoryName ||
mainArticle.categories?.[0]?.title ||
"Berita"}
</span>
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mt-2 leading-snug">
{mainArticle.title}
</h2>
<div className="flex items-center gap-2 text-gray-600 text-xs mt-3">
<span>
By{" "}
{mainArticle.customCreatorName ||
mainArticle.createdByName ||
"Admin"}
</span>
<span></span>
<span>
{new Date(mainArticle.publishedAt).toLocaleDateString(
"id-ID",
{
day: "2-digit",
month: "long",
year: "numeric",
}
)}
</span>
</div>
</div>
</Link>
</div>
) : (
<p className="text-gray-500">Loading...</p>
)}
{/* RIGHT SIDE RECENT POSTS */}
<div>
<h3 className="text-lg font-semibold mb-3">Recent Posts</h3>
<div className="space-y-4">
{recentPosts.map((item) => (
<Link
key={item.id}
href={`/details/${item.slug}`}
className="flex gap-3"
>
<div className="relative w-35 h-25 rounded-md overflow-hidden">
<Image
src={
item.thumbnailUrl ||
item.files?.[0]?.fileUrl ||
"/placeholder.jpg"
}
alt={item.title}
fill
className="object-cover"
/>
</div>
<div className="flex flex-col">
<p className="text-sm font-semibold line-clamp-2">
{item.title}
</p>
<span className="text-xs text-gray-500 mt-1">
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</span>
</div>
</Link>
))}
</div>
</div>
</div>
{/* LOAD MORE */}
<div className="flex justify-center my-6">
<button className="text-gray-600 text-sm flex items-center gap-2 border-b pb-1">
<span>LOAD MORE</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<path
d="M12 5v14M5 12h14"
stroke="#9CA3AF"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</button>
</div>
{/* KOLOM PPS BOTTOM BANNER */}
<div className="relative h-[140px] w-full overflow-hidden rounded-none my-5 border ">
<Image
src="/image-kolom.png"
alt="Kolom PPS Bottom Banner"
fill
className="object-contain"
/>
</div>
</section>
);
}

View File

@ -13,6 +13,7 @@ type Article = {
title: string;
description: string;
categoryName: string;
slug: string;
createdAt: string;
createdByName: string;
thumbnailUrl: string;
@ -98,7 +99,7 @@ export default function Header() {
key={post.id}
className="relative group overflow-hidden h-56 md:h-64"
>
<Link href={`/detail/${post?.id}`}>
<Link href={`/details/${post?.slug}`}>
<Image
src={post.thumbnailUrl || "/default.jpg"}
alt={post.title}
@ -144,7 +145,7 @@ export default function Header() {
key={post.id}
className="relative group overflow-hidden h-56 md:h-64"
>
<Link href={`/detail/${post?.id}`}>
<Link href={`/details/${post?.slug}`}>
<Image
src={post.thumbnailUrl || "/default.jpg"}
alt={post.title}

View File

@ -0,0 +1,186 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { getListArticle } from "@/service/article";
type Article = {
id: number;
title: string;
description: string;
categoryName: string;
createdAt: string;
publishedAt: string;
slug: string;
createdByName: string;
customCreatorName?: string;
thumbnailUrl?: string;
categories?: { title: string }[];
};
export default function NewsTerkini() {
const [articles, setArticles] = useState<Article[]>([]);
const [popular, setPopular] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
async function loadData() {
setLoading(true);
try {
const res = await getListArticle({
limit: "20",
page: 1,
search: "",
isPublish: true,
sort: "desc",
sortBy: "created_at",
});
const data = res?.data?.data || [];
setArticles(data.slice(0, 5));
setPopular(data.slice(0, 5));
} catch (err) {
console.log(err);
}
setLoading(false);
}
const formatDate = (d: string) =>
new Date(d).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
});
if (loading)
return (
<p className="text-center py-10 text-gray-500">
Memuat berita terbaru...
</p>
);
return (
<section className="max-w-7xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
<div>
<h2 className="text-lg font-bold text-gray-900">BERITA TERKINI</h2>
<div className="w-14 h-1 bg-green-600 mt-1 mb-4"></div>
<div className="space-y-6">
{articles.map((item) => (
<Link
key={item.id}
href={`/details/${item.slug}`}
className="block border-b pb-6"
>
<div className="flex gap-4">
<div className="flex-1">
{/* CATEGORY */}
<p className="text-[11px] text-green-700 font-semibold mb-1">
{item.categories?.[0]?.title || "Kategori"}
</p>
{/* JUDUL */}
<h3 className="font-bold text-base leading-snug line-clamp-2">
{item.title}
</h3>
{/* DESKRIPSI */}
<p className="text-sm text-gray-600 line-clamp-2 mt-1">
{item.description}
</p>
{/* AUTHOR + DATE */}
<p className="text-xs text-gray-400 mt-2">
By {item.customCreatorName || item.createdByName} {" "}
{formatDate(item.publishedAt)}
</p>
</div>
<div className="relative w-40 h-28 rounded overflow-hidden flex-shrink-0">
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
fill
className="object-cover"
/>
</div>
</div>
</Link>
))}
</div>
{/* LOAD MORE */}
<div className="text-center mt-4 text-green-600 text-sm cursor-pointer">
LOAD MORE
</div>
</div>
<div className="lg:col-span-1">
<h2 className="text-lg font-bold text-gray-900">TERBANYAK DIBAGIKAN</h2>
<div className="w-14 h-1 bg-green-600 mt-1 mb-4"></div>
<div className="space-y-4">
{popular.map((item, index) => (
<Link
key={item.id}
href={`/details/${item.slug}`}
className="flex gap-3 border-b pb-4"
>
{/* NOMOR */}
<div className="text-green-600 font-extrabold text-3xl leading-none">
{(index + 1).toString().padStart(2, "0")}
</div>
<div className="flex-1">
<p className="text-[10px] text-gray-500">
{item.categories?.[0]?.title || "Kategori"}
</p>
<h4 className="font-semibold text-sm leading-snug line-clamp-2">
{item.title}
</h4>
<p className="text-[10px] text-gray-400 mt-1">
{formatDate(item.createdAt)}
</p>
</div>
{/* THUMBNAIL KECIL */}
<div className="relative w-16 h-14 rounded overflow-hidden">
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
fill
className="object-cover"
/>
</div>
</Link>
))}
</div>
<div className="mt-6">
<div className="relative h-[180px] border rounded-lg overflow-hidden mb-6">
<Image
src="/image-kolom.png"
alt="Kolom PPS"
fill
className="object-contain bg-white"
/>
</div>
<div className="relative h-[180px] border rounded-lg overflow-hidden">
<Image
src="/image-kolom.png"
alt="Kolom PPS"
fill
className="object-contain bg-white"
/>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,192 +1,126 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { getListArticle } from "@/service/article";
import { ChevronDown } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { getAdvertise } from "@/service/advertisement";
import { useEffect, useState } from "react";
type Article = {
id: number;
title: string;
description: string;
categoryName: string;
slug: string;
createdAt: string;
publishedAt: string;
createdByName: string;
customCreatorName: string;
thumbnailUrl: string;
categories: { title: string }[];
files: { fileUrl: string; file_alt: string }[];
};
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
export default function LatestNews() {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("5");
const pathname = usePathname();
const currentSlug = pathname.split("/")[2] || "";
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
const [showData, setShowData] = useState("6");
const [search] = useState("");
const [selectedCategories] = useState<any>("");
const [startDateValue] = useState({
startDate: null,
endDate: null,
});
useEffect(() => {
initStateAdver();
}, []);
initState();
}, [page, showData, startDateValue, selectedCategories]);
async function initStateAdver() {
async function initState() {
const req = {
limit: 100,
page: 1,
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
sortBy: "created_at",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getAdvertise(req);
const data: Advertise[] = res?.data?.data || [1];
// filter iklan dengan placement = "banner"
const banner = data.find((ad) => ad.placement === "jumbotron");
if (banner) {
setBannerAd(banner);
}
} catch (err) {
console.error("Error fetching advertisement:", err);
}
}
useEffect(() => {
fetchArticles();
}, []);
async function fetchArticles() {
try {
const req = {
limit: showData,
page: 1,
search: "",
categorySlug: "",
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
} catch (error) {
console.error("Gagal memuat artikel:", error);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
const mainArticle = articles[0];
const secondArticle = articles[1];
return (
<div className="max-w-7xl mx-auto px-4 py-10 grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<h1 className="text-3xl font-bold mb-2">
{" "}
{currentSlug ? ` ${currentSlug}` : "Berita Terkini"}
</h1>
<p className="text-gray-600 mb-6">
Kolom ini berisi berita-berita yang saat ini sedang menjadi sorotan
atau terkait dengan peristiwa terbaru.
</p>
<section className="max-w-screen-xl mx-auto px-4 py-10">
<div className="">
{/* TITLE */}
<div className="mb-4">
<h2 className="text-xl font-black text-[#000]">BERITA POPULER</h2>
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
</div>
{articles.slice(0, 5).map((article, index) => (
<Link
key={article.id}
href={`/detail/${article.id}`}
className={`grid grid-cols-1 md:grid-cols-2 gap-4 mb-6 items-center`}
>
{/* Gambar */}
<div className={`relative ${index === 0 ? "h-64" : "h-64"} w-full`}>
<Image
src={article.thumbnailUrl}
alt={article.title}
fill
className="object-cover rounded"
/>
</div>
{/* Konten */}
<div className="flex flex-col justify-center">
<span className="bg-yellow-500 text-white px-2 py-1 text-xs font-semibold w-fit mb-2">
BERITA TERKINI
</span>
<h2 className="text-xl font-bold hover:text-red-500 mb-2">
{article.title}
</h2>
<p className="text-xs text-gray-500 mb-2">
by {article.createdByName} {formatDate(article.createdAt)}
💬 0
</p>
<p className="text-sm text-gray-700 mb-3 line-clamp-3">
{article.description}
</p>
<button className="bg-black text-white text-xs px-4 py-2 w-fit hover:bg-gray-800">
READ MORE
</button>
</div>
</Link>
))}
</div>
{/* KANAN: Advertisement */}
<div className="w-full">
<div className="border p-4 bg-white flex flex-col items-center">
<p className="text-center text-sm text-gray-600 mb-2">
Smart & Responsive
</p>
<h3 className="text-center text-md font-bold mb-4">ADVERTISEMENT</h3>
<div className="relative w-full h-[300px] bg-gray-100">
{bannerAd ? (
<a
href={bannerAd.redirectLink}
target="_blank"
rel="noopener noreferrer"
className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
{/* GRID 4 KOLOM */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{articles.slice(0, 4).map((item) => (
<Link href={`/details/${item.slug}`} key={item.id}>
<div>
{/* GAMBAR */}
<div className="relative w-full h-56 rounded-lg overflow-hidden">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
fill
className="object-cover"
/>
{/* BADGE CATEGORY DI DALAM GAMBAR */}
<div className="absolute bottom-2 left-2">
<span className="px-3 py-1 text-[10px] font-semibold bg-green-600 text-white rounded">
{item.categories?.[0]?.title || "Kategori"}
</span>
</div>
</div>
</a>
) : (
<Image
src="/kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div>
<button className="mt-4 text-xs bg-black text-white px-4 py-2 hover:bg-gray-800">
LEARN MORE
</button>
{/* JUDUL */}
<h3 className="mt-2 text-base font-bold leading-snug line-clamp-2">
{item.title}
</h3>
{/* AUTHOR + DATE */}
<div className="text-[11px] mt-2 flex items-center gap-1 text-gray-700">
<span className="font-semibold">
By {item.customCreatorName || item.createdByName || "Admin"}
</span>
<span className="text-yellow-500">-</span>
<span>
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
</div>
</div>
</Link>
))}
</div>
<div className="relative h-[160px] w-full overflow-hidden rounded-xl mt-6">
<Image
src="/image-kolom.png"
alt="Kolom PPS"
fill
className="object-contain bg-white"
/>
</div>
</div>
</div>
</section>
);
}
function formatDate(dateStr: string): string {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
return new Date(dateStr).toLocaleDateString("id-ID", options);
}

View File

@ -1,56 +1,126 @@
// components/landing-page/navbar.tsx
import Image from "next/image";
"use client";
import { Search } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "../ui/button";
export default function Navbar() {
return (
<header>
{/* Top section dengan logo */}
<div className="bg-[#3972ab] flex justify-center py-6">
<Image
src="/harapan.png" // ganti dengan path logo kamu
alt="Kabar Harapan"
width={200}
height={80}
priority
/>
</div>
<div className="w-full bg-white py-4 border-b">
<div className="max-w-screen-xl mx-auto flex flex-col justify-between px-4">
{/* Left: Logo */}
<div className="flex flex-row justify-between mb-3">
<div className="flex items-center">
<Image
src="/harapan.png"
alt="Kritik Tajam Logo"
width={140}
height={100}
/>
</div>
<div className="flex items-center gap-6">
{/* Social Icons */}
<div className="hidden md:flex items-center gap-5 text-black text-xl">
<Link href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
/>
</svg>
</Link>
{/* Navbar menu */}
<nav className="bg-[#3a75a9] border-t border-[#427cae]">
<div className="max-w-5xl mx-auto flex items-center justify-center gap-8 py-3 relative">
<a
href="/"
className="text-black text-sm font-medium hover:underline"
>
HOME
</a>
<a
href="/category/latest-news"
className="text-black text-sm font-medium hover:underline"
>
BERITA TERKINI
</a>
<a
href="/category/popular-news"
className="text-black text-sm font-medium hover:underline"
>
BERITA TERPOPULER
</a>
<Link href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M7.91 20.889c8.302 0 12.845-6.885 12.845-12.845c0-.193 0-.387-.009-.58A9.2 9.2 0 0 0 23 5.121a9.2 9.2 0 0 1-2.597.713a4.54 4.54 0 0 0 1.99-2.5a9 9 0 0 1-2.87 1.091A4.5 4.5 0 0 0 16.23 3a4.52 4.52 0 0 0-4.516 4.516c0 .352.044.696.114 1.03a12.82 12.82 0 0 1-9.305-4.718a4.526 4.526 0 0 0 1.4 6.03a4.6 4.6 0 0 1-2.043-.563v.061a4.524 4.524 0 0 0 3.62 4.428a4.4 4.4 0 0 1-1.189.159q-.435 0-.845-.08a4.51 4.51 0 0 0 4.217 3.135a9.05 9.05 0 0 1-5.608 1.936A9 9 0 0 1 1 18.873a12.84 12.84 0 0 0 6.91 2.016"
/>
</svg>
</Link>
{/* Search icon di kanan */}
<div className="absolute right-4 flex items-center gap-2">
<Search className="w-5 h-5 text-white cursor-pointer" />
<Link href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4zm9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8A1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5a5 5 0 0 1-5 5a5 5 0 0 1-5-5a5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3"
/>
</svg>
</Link>
<Link href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12.244 4c.534.003 1.87.016 3.29.073l.504.022c1.429.067 2.857.183 3.566.38c.945.266 1.687 1.04 1.938 2.022c.4 1.56.45 4.602.456 5.339l.001.152v.174c-.007.737-.057 3.78-.457 5.339c-.254.985-.997 1.76-1.938 2.022c-.709.197-2.137.313-3.566.38l-.504.023c-1.42.056-2.756.07-3.29.072l-.235.001h-.255c-1.13-.007-5.856-.058-7.36-.476c-.944-.266-1.687-1.04-1.938-2.022c-.4-1.56-.45-4.602-.456-5.339v-.326c.006-.737.056-3.78.456-5.339c.254-.985.997-1.76 1.939-2.021c1.503-.419 6.23-.47 7.36-.476zM9.999 8.5v7l6-3.5z"
/>
</svg>
</Link>
</div>
</div>
</div>
{/* Middle Menu */}
<div className="flex flex-row justify-between">
<nav className="hidden md:flex items-center gap-10 text-sm font-semibold">
<Link href="/" className="text-blue-500 underline">
Beranda
</Link>
<Link href="/category/latest-news">BERITA TERKINI</Link>
<Link href="/category/popular-news">BERITA TERPOPULER</Link>
</nav>
<div className="flex items-center gap-2">
<button
// onClick={() => document.documentElement.classList.toggle("dark")}
className="w-10 h-5 rounded-full bg-gray-300 dark:bg-gray-700 relative transition-all"
>
<div className="w-5 h-5 bg-white dark:bg-black rounded-full shadow absolute top-0 left-0 dark:left-5 transition-all"></div>
</button>
{/* BURGER BUTTON (mobile menu) */}
<button className="md:hidden p-2 rounded-lg border">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<button className="p-2 border rounded-full">
<Search size={15} />
</button>
<Link href={"/auth"}>
<Button className="bg-black text-white rounded-md px-5 py-2 hover:bg-yellow-500">
Login
</Button>
<button className="bg-green-600 text-white px-5 py-2 rounded-full text-sm font-semibold">
LOGIN
</button>
</Link>
</div>
</div>
</nav>
</header>
</div>
</div>
);
}

View File

@ -12,6 +12,7 @@ type Article = {
description: string;
categoryName: string;
createdAt: string;
slug: string;
createdByName: string;
thumbnailUrl: string;
categories: { title: string }[];
@ -221,7 +222,10 @@ export default function News() {
function PostItem({ post }: { post: Article }) {
return (
<div className="flex gap-4 mb-6 pb-4">
<Link className="flex items-center gap-4 " href={`/detail/${post?.id}`}>
<Link
className="flex items-center gap-4 "
href={`/details/${post?.slug}`}
>
<div className="relative w-40 h-28 flex-shrink-0">
<Image
src={

View File

@ -0,0 +1,126 @@
"use client";
import { getListArticle } from "@/service/article";
import { ChevronDown } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
type Article = {
id: number;
title: string;
description: string;
categoryName: string;
slug: string;
createdAt: string;
publishedAt: string;
createdByName: string;
customCreatorName: string;
thumbnailUrl: string;
categories: { title: string }[];
files: { fileUrl: string; file_alt: string }[];
};
export default function OpinionNews() {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("6");
const [search] = useState("");
const [selectedCategories] = useState<any>("");
const [startDateValue] = useState({
startDate: null,
endDate: null,
});
useEffect(() => {
initState();
}, [page, showData, startDateValue, selectedCategories]);
async function initState() {
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
return (
<section className="max-w-screen-xl mx-auto px-4 py-10">
<div className="">
{/* TITLE */}
<div className="mb-4">
<h2 className="text-xl font-black text-[#000]">BERITA OPINI</h2>
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
</div>
{/* GRID 4 KOLOM */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{articles.slice(0, 4).map((item) => (
<Link href={`/details/${item.slug}`} key={item.id}>
<div>
{/* GAMBAR */}
<div className="relative w-full h-56 rounded-lg overflow-hidden">
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
fill
className="object-cover"
/>
{/* BADGE CATEGORY DI DALAM GAMBAR */}
<div className="absolute bottom-2 left-2">
<span className="px-3 py-1 text-[10px] font-semibold bg-green-600 text-white rounded">
{item.categories?.[0]?.title || "Kategori"}
</span>
</div>
</div>
{/* JUDUL */}
<h3 className="mt-2 text-base font-bold leading-snug line-clamp-2">
{item.title}
</h3>
{/* AUTHOR + DATE */}
<div className="text-[11px] mt-2 flex items-center gap-1 text-gray-700">
<span className="font-semibold">
By {item.customCreatorName || item.createdByName || "Admin"}
</span>
<span className="text-yellow-500">-</span>
<span>
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
</div>
</div>
</Link>
))}
</div>
<div className="relative h-[160px] w-full overflow-hidden rounded-xl mt-6">
<Image
src="/image-kolom.png"
alt="Kolom PPS"
fill
className="object-contain bg-white"
/>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,151 @@
import { Eye, Heart, MessageCircle } from "lucide-react";
import Image from "next/image";
interface VideoItem {
id: number;
title: string;
thumbnail: string;
duration: string;
publishedAt: string;
views: string;
likes: string;
comments: string;
}
const sampleVideos: VideoItem[] = [
{
id: 1,
title: "Cuplikan Kegiatan Presiden di IKN",
thumbnail: "/yt/thumb1.jpg",
duration: "12:30",
publishedAt: "2 jam yang lalu",
views: "12K",
likes: "230",
comments: "45",
},
{
id: 2,
title: "Pembangunan MRT Fase Berikutnya Resmi Dimulai",
thumbnail: "/yt/thumb2.jpg",
duration: "08:12",
publishedAt: "5 jam yang lalu",
views: "9.4K",
likes: "180",
comments: "30",
},
{
id: 3,
title: "Wilayah Indonesia Siap Hadapi Cuaca Ekstrem",
thumbnail: "/yt/thumb3.jpg",
duration: "05:50",
publishedAt: "1 hari lalu",
views: "21K",
likes: "540",
comments: "121",
},
{
id: 4,
title: "Peningkatan Ekonomi Regional Terus Dijaga",
thumbnail: "/yt/thumb4.jpg",
duration: "10:44",
publishedAt: "2 hari lalu",
views: "18K",
likes: "420",
comments: "88",
},
{
id: 5,
title: "Laporan Khusus Perkembangan Industri Kreatif",
thumbnail: "/yt/thumb5.jpg",
duration: "14:02",
publishedAt: "3 hari lalu",
views: "30K",
likes: "830",
comments: "200",
},
{
id: 6,
title: "Paparan Menteri Perhubungan Soal Transportasi",
thumbnail: "/yt/thumb6.jpg",
duration: "07:21",
publishedAt: "4 hari lalu",
views: "9K",
likes: "114",
comments: "26",
},
];
export default function YouTubeSection() {
return (
<section className="max-w-7xl mx-auto px-4 py-8">
{/* Header Channel */}
<div className="flex items-center gap-4 mb-6">
<Image
src="/yt/channel-logo.png"
alt="Logo Channel"
width={70}
height={70}
className="rounded-full"
/>
<div>
<h2 className="text-xl font-semibold">YouTube Channel Resmi</h2>
<p className="text-sm text-gray-600">
Lebih dari 3.5 juta pengikut 12k video
</p>
</div>
<a
href="#"
className="ml-auto bg-red-600 text-white px-4 py-2 rounded-lg font-semibold"
>
Subscribe
</a>
</div>
{/* Grid Video */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{sampleVideos.map((v) => (
<div key={v.id} className="cursor-pointer">
<div className="relative w-full h-44">
<Image
src={v.thumbnail}
alt={v.title}
fill
className="rounded-lg object-cover"
/>
<span className="absolute bottom-1 right-1 bg-black/80 text-white text-xs px-2 py-1 rounded">
{v.duration}
</span>
</div>
<h3 className="font-semibold mt-2 leading-tight line-clamp-2">
{v.title}
</h3>
<p className="text-sm text-gray-600">{v.publishedAt}</p>
<div className="flex items-center gap-4 text-sm text-gray-700 mt-1">
<span className="flex items-center gap-1">
<Eye size={16} /> {v.views}
</span>
<span className="flex items-center gap-1">
<Heart size={16} /> {v.likes}
</span>
<span className="flex items-center gap-1">
<MessageCircle size={16} /> {v.comments}
</span>
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex justify-center mt-8">
<button className="px-5 py-2 rounded-lg border hover:bg-gray-100">
Lihat Selanjutnya
</button>
</div>
</section>
);
}

View File

@ -199,11 +199,11 @@ export default function ArticleTable() {
initState();
};
const copyUrlArticle = async (id: number) => {
const copyUrlArticle = async (slug: any) => {
const url =
`${window.location.protocol}//${window.location.host}` +
"/detail/" +
`${id}`;
"/details/" +
`${slug}`;
try {
await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard");
@ -263,7 +263,9 @@ export default function ArticleTable() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => copyUrlArticle(article.id)}>
<DropdownMenuItem
onClick={() => copyUrlArticle(article.slug)}
>
<CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article
</DropdownMenuItem>
@ -314,7 +316,7 @@ export default function ArticleTable() {
return cellValue;
}
},
[article, page]
[article, page],
);
let typingTimer: NodeJS.Timeout;
@ -443,8 +445,8 @@ export default function ArticleTable() {
</div>
</div>
<div className="w-full overflow-x-hidden">
<div className="w-full mx-auto overflow-x-hidden">
<Table className="w-full table-fixed border text-sm">
<div className="w-full overflow-x-auto">
<Table className="min-w-[1000px] w-full table-auto border text-sm">
<TableHeader>
<TableRow>
{(username === "admin-mabes"
@ -453,7 +455,18 @@ export default function ArticleTable() {
).map((column) => (
<TableHead
key={column.uid}
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
className={`bg-white dark:bg-black text-black dark:text-white
text-sm font-semibold border-b px-3 py-3
${
column.uid === "no"
? "min-w-[60px] text-center"
: column.uid === "title"
? "min-w-[280px]"
: column.uid === "actions"
? "min-w-[100px] text-center"
: "min-w-[160px]"
}
`}
>
{column.name}
</TableHead>
@ -470,7 +483,17 @@ export default function ArticleTable() {
).map((column) => (
<TableCell
key={column.uid}
className="truncate text-black dark:text-white max-w-[200px]"
className={`text-black dark:text-white text-sm px-3 py-3 align-top
${
column.uid === "no"
? "min-w-[60px] text-center font-medium"
: column.uid === "title"
? "min-w-[280px] whitespace-normal break-words leading-snug"
: column.uid === "actions"
? "min-w-[100px] text-center"
: "min-w-[160px] whitespace-normal break-words"
}
`}
>
{renderCell(item, column.uid)}
</TableCell>

View File

@ -1,13 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["mikulnews.com", "dev.mikulnews.com"],
},
eslint: {
ignoreDuringBuilds: true,
},
// Add experimental features for better chunk handling
experimental: {
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
},

262
package-lock.json generated
View File

@ -37,12 +37,12 @@
"js-cookie": "^3.0.5",
"lightningcss": "^1.30.1",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"react": "19.1.0",
"next": "^16.1.1",
"react": "^19.2.3",
"react-apexcharts": "^1.7.0",
"react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0",
"react-dom": "19.1.0",
"react-dom": "^19.2.4",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.59.0",
"react-password-checklist": "^1.8.1",
@ -8961,9 +8961,9 @@
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
"integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
@ -9037,9 +9037,9 @@
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="
},
"node_modules/@next/env": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz",
"integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw=="
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "15.5.3",
@ -9050,115 +9050,10 @@
"fast-glob": "3.3.1"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
"integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
"integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
"integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
"integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
"integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
"integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
"integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
"integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
"integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [
"x64"
],
@ -11044,6 +10939,14 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
@ -11126,9 +11029,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001743",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
"integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
"version": "1.0.30001761",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"funding": [
{
"type": "opencollective",
@ -11674,9 +11577,9 @@
}
},
"node_modules/detect-libc": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"engines": {
"node": ">=8"
}
@ -14772,12 +14675,13 @@
"dev": true
},
"node_modules/next": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz",
"integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==",
"version": "16.1.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"dependencies": {
"@next/env": "15.5.3",
"@next/env": "16.1.1",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@ -14786,18 +14690,18 @@
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.3",
"@next/swc-darwin-x64": "15.5.3",
"@next/swc-linux-arm64-gnu": "15.5.3",
"@next/swc-linux-arm64-musl": "15.5.3",
"@next/swc-linux-x64-gnu": "15.5.3",
"@next/swc-linux-x64-musl": "15.5.3",
"@next/swc-win32-arm64-msvc": "15.5.3",
"@next/swc-win32-x64-msvc": "15.5.3",
"sharp": "^0.34.3"
"@next/swc-darwin-arm64": "16.1.1",
"@next/swc-darwin-x64": "16.1.1",
"@next/swc-linux-arm64-gnu": "16.1.1",
"@next/swc-linux-arm64-musl": "16.1.1",
"@next/swc-linux-x64-gnu": "16.1.1",
"@next/swc-linux-x64-musl": "16.1.1",
"@next/swc-win32-arm64-msvc": "16.1.1",
"@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -15204,9 +15108,9 @@
]
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"engines": {
"node": ">=0.10.0"
}
@ -15258,14 +15162,14 @@
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"dependencies": {
"scheduler": "^0.26.0"
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.1.0"
"react": "^19.2.4"
}
},
"node_modules/react-dropzone": {
@ -15711,14 +15615,14 @@
}
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"devOptional": true,
"bin": {
"semver": "bin/semver.js"
@ -15774,15 +15678,15 @@
}
},
"node_modules/sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
"integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.0",
"semver": "^7.7.2"
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@ -15791,28 +15695,30 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.4",
"@img/sharp-darwin-x64": "0.34.4",
"@img/sharp-libvips-darwin-arm64": "1.2.3",
"@img/sharp-libvips-darwin-x64": "1.2.3",
"@img/sharp-libvips-linux-arm": "1.2.3",
"@img/sharp-libvips-linux-arm64": "1.2.3",
"@img/sharp-libvips-linux-ppc64": "1.2.3",
"@img/sharp-libvips-linux-s390x": "1.2.3",
"@img/sharp-libvips-linux-x64": "1.2.3",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
"@img/sharp-libvips-linuxmusl-x64": "1.2.3",
"@img/sharp-linux-arm": "0.34.4",
"@img/sharp-linux-arm64": "0.34.4",
"@img/sharp-linux-ppc64": "0.34.4",
"@img/sharp-linux-s390x": "0.34.4",
"@img/sharp-linux-x64": "0.34.4",
"@img/sharp-linuxmusl-arm64": "0.34.4",
"@img/sharp-linuxmusl-x64": "0.34.4",
"@img/sharp-wasm32": "0.34.4",
"@img/sharp-win32-arm64": "0.34.4",
"@img/sharp-win32-ia32": "0.34.4",
"@img/sharp-win32-x64": "0.34.4"
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {

View File

@ -38,12 +38,12 @@
"js-cookie": "^3.0.5",
"lightningcss": "^1.30.1",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"react": "19.1.0",
"next": "^16.1.1",
"react": "^19.2.3",
"react-apexcharts": "^1.7.0",
"react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0",
"react-dom": "19.1.0",
"react-dom": "^19.2.4",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.59.0",
"react-password-checklist": "^1.8.1",

View File

@ -106,6 +106,13 @@ export async function getArticleById(id: any) {
return await httpGet(`/articles/${id}`, headers);
}
export async function getArticleBySlug(slug: any) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/articles/slug/${slug}`, headers);
}
export async function deleteArticle(id: string) {
const headers = {
"content-type": "application/json",

View File

@ -1,6 +1,6 @@
import axios from "axios";
const baseURL = "https://dev.mikulnews.com/api";
const baseURL = "https://kabarharapan.com/api";
const axiosBaseInstance = axios.create({
baseURL,

View File

@ -2,7 +2,7 @@ import axios from "axios";
import { postSignIn } from "../master-user";
import Cookies from "js-cookie";
const baseURL = "https://dev.mikulnews.com/api";
const baseURL = "https://kabarharapan.com/api";
const refreshToken = Cookies.get("refresh_token");
@ -28,7 +28,7 @@ axiosInterceptorInstance.interceptors.request.use(
},
(error) => {
return Promise.reject(error);
}
},
);
// Response interceptor
@ -66,7 +66,7 @@ axiosInterceptorInstance.interceptors.response.use(
}
return Promise.reject(error);
}
},
);
export default axiosInterceptorInstance;

121
styles/custom-editor.css Normal file
View File

@ -0,0 +1,121 @@
/* ========== CKEditor Wrapper ========== */
.ckeditor-wrapper {
border-radius: 6px;
overflow: hidden;
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
/* ========== Main Editor Container ========== */
.ckeditor-wrapper .ck.ck-editor__main {
min-height: var(--editor-min-height, 400px);
max-height: var(--editor-max-height, 600px);
}
/* ========== Editable Content Area (ClassicEditor) ========== */
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline {
min-height: calc(var(--editor-min-height, 400px) - 50px);
max-height: calc(var(--editor-max-height, 600px) - 50px);
overflow-y: auto !important;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
background: #fff !important;
color: #111 !important;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
padding: 1rem;
border: none !important;
}
/* ========== Headings and Text Formatting ========== */
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h1,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h2,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h3,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h4,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h5,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h6 {
margin: 1em 0 0.5em 0;
color: inherit !important;
}
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline p {
margin: 0.5em 0 !important;
}
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline ul,
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline ol {
margin: 0.5em 0;
padding-left: 2em;
}
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline blockquote {
margin: 1em 0;
padding: 0.5em 1em;
border-left: 4px solid #d1d5db;
background-color: #f9fafb;
color: inherit !important;
}
/* ========== Dark Mode Support ========== */
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline {
background: #111 !important;
color: #f9fafb !important;
}
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h1,
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h2,
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h3,
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h4,
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h5,
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h6 {
color: #f9fafb !important;
}
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline blockquote {
background-color: #1f2937 !important;
border-left-color: #374151 !important;
color: #f3f4f6 !important;
}
/* ========== Custom Scrollbars (Light & Dark) ========== */
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar {
width: 8px;
}
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.dark
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-track {
background: #1f2937;
}
.dark
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb {
background: #4b5563;
}
.dark
.ckeditor-wrapper
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}

View File

@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}