Compare commits

..

36 Commits

Author SHA1 Message Date
Anang Yusman b849ecf785 fix drone
continuous-integration/drone/push Build encountered an error Details
2026-03-04 16:27:37 +08:00
Anang Yusman b0aa58118b fix again drone
continuous-integration/drone/push Build encountered an error Details
2026-03-04 16:22:54 +08:00
Anang Yusman 897bd7d504 fix drone
continuous-integration/drone/push Build encountered an error Details
2026-03-04 16:19:06 +08:00
Anang Yusman f8efd7a45e fix drone
continuous-integration/drone/push Build is failing Details
2026-03-04 16:12:18 +08:00
Anang Yusman 6ef0cc29a6 update service 2026-03-02 16:30:07 +08:00
Anang Yusman 58735a6343 update ckeditor 2026-02-10 15:37:24 +08:00
Anang Yusman ce45ed8bdc update landingpage 2026-02-01 22:38:20 +08:00
Anang Yusman 1b8ef91a72 fix:update publishedAt detail 2026-01-23 15:23:37 +08:00
Anang Yusman b287069d30 update 2026-01-13 17:52:13 +08:00
Anang Yusman 40c79b9ae2 update 2026-01-06 10:25:00 +08:00
Anang Yusman 2747e9fcd4 update 2025-12-28 18:47:39 +08:00
Anang Yusman f742115d96 update 2025-12-24 17:58:53 +08:00
Anang Yusman 5bfdc59f83 update 2025-12-23 16:55:53 +08:00
Anang Yusman 6270f23871 update 2025-12-18 11:40:09 +08:00
Anang Yusman 19e1b7dfbd update 2025-12-15 11:14:09 +08:00
Anang Yusman 50802e140b update 2025-12-15 11:13:11 +08:00
hanif salafi 21d2583120 Edit .gitlab-ci.yml 2025-12-14 16:51:10 +00:00
Anang Yusman db571585c2 update ci 2025-12-11 14:14:33 +08:00
Anang Yusman 435222a792 update 2025-11-03 10:17:06 +08:00
Anang Yusman d6d72454eb update 2025-10-28 14:28:53 +08:00
Anang Yusman 36d8790cb6 update 2025-10-28 14:21:49 +08:00
Anang Yusman 0351a30dcb update 2025-10-28 14:14:21 +08:00
Anang Yusman 25740a50d4 update 2025-10-28 13:38:49 +08:00
Anang Yusman d780833116 update 2025-10-24 15:35:10 +08:00
Anang Yusman 522245f6dc fix 2025-10-21 13:15:38 +08:00
Anang Yusman 6fb6e977e9 update 2025-10-17 16:31:33 +08:00
Anang Yusman 87e391a5bf update 2025-10-15 13:49:17 +08:00
Anang Yusman db08a8051a update 2025-10-13 15:57:56 +08:00
Anang Yusman 7ced42966b update 2025-10-12 23:09:52 +08:00
Anang Yusman 85a8275eca update 2025-10-02 15:00:24 +08:00
Anang Yusman 3abbe66b93 update 2025-09-29 17:15:42 +08:00
Anang Yusman a7895fc396 update copy link 2025-09-26 17:27:07 +08:00
Anang Yusman 6c28fd3a94 update edit dan detail 2025-09-26 14:21:15 +08:00
Anang Yusman 5236d4d9d5 update 2025-09-24 17:01:07 +08:00
Anang Yusman 970221ef64 update 2025-09-22 13:06:40 +08:00
Anang Yusman 3a906a755b fix 2025-09-19 16:07:58 +08:00
9910 changed files with 929062 additions and 27996 deletions

42
.drone.yml Normal file
View File

@ -0,0 +1,42 @@
kind: pipeline
type: ssh
name: mikul-news-build-deploy
server:
host:
from_secret: ssh_host
user:
from_secret: ssh_user
ssh_key:
from_secret: ssh_key
steps:
- name: prepare repo
when:
branch:
- main
commands:
- rm -rf /opt/build/web-mikul-news
- mkdir -p /opt/build
- cd /opt/build
- git clone http://38.47.180.165:3000/medol/web-mikul-news.git
- name: build image
when:
branch:
- main
commands:
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
- cd /opt/build/web-mikul-news
- docker build -t 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH .
- docker push 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH
- name: deploy
when:
branch:
- main
commands:
- docker pull 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH
- docker stop web-mikul-news || true
- docker rm web-mikul-news || true
- docker run -dt -p 4600:3000 --restart always --name web-mikul-news 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH

View File

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

View File

@ -12,7 +12,7 @@ import Image from "next/image";
export default function Development() { export default function Development() {
return ( return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]"> <div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="fixed top-0 left-0 w-full h-auto z-0"> {/* <div className="fixed top-0 left-0 w-full h-auto z-0">
<Image <Image
src="/rumput.jpg" src="/rumput.jpg"
alt="Background" alt="Background"
@ -21,7 +21,7 @@ export default function Development() {
className="w-full h-auto object-cover" className="w-full h-auto object-cover"
priority priority
/> />
</div> </div> */}
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto"> <div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar /> <Navbar />

View File

@ -8,7 +8,7 @@ import Image from "next/image";
export default function Development() { export default function Development() {
return ( return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]"> <div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="fixed top-0 left-0 w-full h-auto z-0"> {/* <div className="fixed top-0 left-0 w-full h-auto z-0">
<Image <Image
src="/rumput.jpg" src="/rumput.jpg"
alt="Background" alt="Background"
@ -17,7 +17,7 @@ export default function Development() {
className="w-full h-auto object-cover" className="w-full h-auto object-cover"
priority priority
/> />
</div> </div> */}
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto"> <div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar /> <Navbar />

View File

@ -13,7 +13,7 @@ import Image from "next/image";
export default function Development() { export default function Development() {
return ( return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]"> <div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="fixed top-0 left-0 w-full h-auto z-0"> {/* <div className="fixed top-0 left-0 w-full h-auto z-0">
<Image <Image
src="/rumput.jpg" src="/rumput.jpg"
alt="Background" alt="Background"
@ -22,7 +22,7 @@ export default function Development() {
className="w-full h-auto object-cover" className="w-full h-auto object-cover"
priority priority
/> />
</div> </div> */}
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto"> <div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar /> <Navbar />

View File

@ -6,7 +6,7 @@ import Image from "next/image";
export default function Home() { export default function Home() {
return ( return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]"> <div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="fixed top-0 left-0 w-full h-auto z-0"> {/* <div className="fixed top-0 left-0 w-full h-auto z-0">
<Image <Image
src="/rumput.jpg" src="/rumput.jpg"
alt="Background" alt="Background"
@ -15,7 +15,7 @@ export default function Home() {
className="w-full h-auto object-cover" className="w-full h-auto object-cover"
priority priority
/> />
</div> </div> */}
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto"> <div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar /> <Navbar />

View File

@ -0,0 +1,30 @@
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 Home() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="fixed top-0 left-0 w-full h-auto z-0">
<Image
src="/rumput.jpg"
alt="Background"
width={1450}
height={600}
className="w-full h-auto object-cover"
priority
/>
</div>
<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,39 +1,26 @@
import Author from "@/components/landing-page/author";
import Footer from "@/components/landing-page/footer"; import Footer from "@/components/landing-page/footer";
import Header from "@/components/landing-page/header"; import Header from "@/components/landing-page/header";
import Latest from "@/components/landing-page/latest";
import LatestandPopular from "@/components/landing-page/latest-and-popular";
import Navbar from "@/components/landing-page/navbar"; import Navbar from "@/components/landing-page/navbar";
import News from "@/components/landing-page/news"; import LatestNews from "@/components/landing-page/latest-news";
import { id } from "date-fns/locale"; import Development from "@/components/landing-page/development";
import Image from "next/image"; 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() { export default function Home() {
return ( return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]"> <div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
{/* Background fixed tidak ikut scroll */} {/* Background fixed tidak ikut scroll */}
<div className="fixed top-0 left-0 w-full h-auto z-0"> <Navbar />
<Image <div className="flex-1">
src="/rumput.jpg" <Header />
alt="Background"
width={1450}
height={600}
className="w-full h-auto object-cover"
priority
/>
</div>
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar />
<div className="flex-1">
<Header />
</div>
<Latest id={2} />
<News />
<Author />
<LatestandPopular />
<Footer />
</div> </div>
<LatestNews />
<Development />
<OpinionNews />
<NewsTerkini />
<YouTubeSection />
<Footer />
</div> </div>
); );
} }

View File

@ -3,25 +3,46 @@ import Image from "next/image";
import Author from "../landing-page/author"; import Author from "../landing-page/author";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { getArticleById, getListArticle } from "@/service/article"; import {
import { close, loading } from "@/config/swal"; getArticleById,
import { useParams } from "next/navigation"; getArticleBySlug,
getArticleFiles,
getListArticle,
} from "@/service/article";
import { close, error, loading } from "@/config/swal";
import { useParams, usePathname } from "next/navigation";
import { Link2, MailIcon } from "lucide-react";
import { getAdvertise } from "@/service/advertisement";
import { saveActivity } from "@/service/activity-log";
import {
getArticleComment,
otpRequest,
otpValidation,
postArticleComment,
} from "@/service/master-user";
import { useForm } from "react-hook-form";
import { Badge } from "../ui/badge";
import { map } from "zod";
import { formatTextToHtmlTag } from "@/utils/global";
type TabKey = "trending" | "comments" | "latest"; type TabKey = "trending" | "comments" | "latest";
type Article = { type Article = {
id: number; id: number;
slug: string;
title: string; title: string;
description: string; description: string;
htmlDescription: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -32,9 +53,20 @@ interface CategoryType {
value: number; value: number;
} }
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
export default function DetailContent() { export default function DetailContent() {
const params = useParams(); const params = useParams();
const id = params?.id; const id = params?.id;
const slug = params?.slug;
const pathname = usePathname();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1); const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]); const [articles, setArticles] = useState<Article[]>([]);
@ -47,18 +79,82 @@ export default function DetailContent() {
startDate: null, startDate: null,
endDate: null, endDate: null,
}); });
const [detailfiles, setDetailFiles] = useState<any>([]); // const [detailfiles, setDetailFiles] = useState<any>([]);
const [detailFiles, setDetailFiles] = useState<any[]>([]);
const [mainImage, setMainImage] = useState(0); const [mainImage, setMainImage] = useState(0);
const [thumbnail, setThumbnail] = useState("-"); const [thumbnail, setThumbnail] = useState("-");
const [diseId, setDiseId] = useState(0); const [diseId, setDiseId] = useState(0);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]); const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number | null>( const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null null,
); );
const [selectedIndex, setSelectedIndex] = useState(0);
const [tabArticles, setTabArticles] = useState<Article[]>([]); const [tabArticles, setTabArticles] = useState<Article[]>([]);
const [activeTab, setActiveTab] = useState<TabKey>("trending"); const [activeTab, setActiveTab] = useState<TabKey>("trending");
const [needOtp, setNeedOtp] = useState(false);
const [otpValue, setOtpValue] = useState("");
const { register, handleSubmit, reset, watch } = useForm();
const [commentList, setCommentList] = useState<any>([]);
useEffect(() => {
fetchData();
}, [id]);
const fetchData = async () => {
try {
const res = await getArticleComment(String(id));
const data = res?.data?.data;
setCommentList(data);
console.log("komen", data);
} catch (err) {
console.error("❌ Gagal memuat komentar:", err);
setCommentList([]);
}
};
const onSubmit = async (values: any) => {
if (!needOtp) {
const res = await otpRequest(values.email, values?.name);
if (res?.error) {
error(res.message);
return false;
}
setNeedOtp(true);
} else {
const validation = await otpValidation(values.email, otpValue);
if (validation?.error) {
error("OTP Tidak Sesuai");
return false;
}
const data = {
commentFromName: values.name,
commentFromEmail: values.email,
articleId: Number(id),
isPublic: false,
message: values.comment,
parentId: 0,
};
const res = await postArticleComment(data);
if (res?.error) {
error(res?.message);
return false;
}
const req: any = {
activityTypeId: 5,
url: "https://dev.mikulnews/" + pathname,
articleId: Number(id),
};
const resActivity = await saveActivity(req);
reset();
fetchData();
setNeedOtp(false);
}
};
const tabs: { id: TabKey; label: string }[] = [ const tabs: { id: TabKey; label: string }[] = [
{ id: "trending", label: "Trending" }, { id: "trending", label: "Trending" },
@ -66,6 +162,35 @@ export default function DetailContent() {
{ id: "latest", label: "Latest" }, { id: "latest", label: "Latest" },
]; ];
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
useEffect(() => {
initStateAdver();
}, []);
async function initStateAdver() {
const req = {
limit: 100,
page: 1,
sort: "desc",
sortBy: "created_at",
isPublish: true,
};
try {
const res = await getAdvertise(req);
const data: Advertise[] = res?.data?.data || [1];
const banner = data.find((ad) => ad.placement === "jumbotron");
if (banner) {
setBannerAd(banner);
}
} catch (err) {
console.error("Error fetching advertisement:", err);
}
}
useEffect(() => { useEffect(() => {
fetchTabArticles(); fetchTabArticles();
}, [activeTab]); }, [activeTab]);
@ -80,7 +205,6 @@ export default function DetailContent() {
sortBy: "created_at", sortBy: "created_at",
}; };
// Sesuaikan sortBy berdasarkan tab
if (activeTab === "trending") { if (activeTab === "trending") {
req.sortBy = "view_count"; req.sortBy = "view_count";
} else if (activeTab === "comments") { } else if (activeTab === "comments") {
@ -89,7 +213,6 @@ export default function DetailContent() {
req.sortBy = "created_at"; req.sortBy = "created_at";
} }
// Hanya kirimkan search jika valid
if (search && search !== "-" && search !== "") { if (search && search !== "-" && search !== "") {
req.search = search; req.search = search;
} }
@ -180,16 +303,48 @@ export default function DetailContent() {
initStateData(); initStateData();
}, [listCategory]); }, [listCategory]);
useEffect(() => {
setSelectedIndex(0);
}, [detailFiles]);
async function initStateData() { async function initStateData() {
loading(); loading();
const res = await getArticleById(id); try {
const data = res.data?.data; // 1⃣ Ambil artikel by slug
const res = await getArticleBySlug(slug);
const data = res?.data?.data;
setThumbnail(data?.thumbnailUrl); if (!data?.id) return;
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files); setArticleDetail(data);
setArticleDetail(data); // <-- Add this setThumbnail(data.thumbnailUrl);
close(); setDiseId(data.aiArticleId);
// 2⃣ Ambil SEMUA article files
const filesRes = await getArticleFiles();
const allFiles = filesRes?.data?.data ?? [];
// 3⃣ FILTER sesuai articleId
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === data.id,
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state detail error:", error);
} finally {
close();
}
}
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 ( return (
@ -232,39 +387,69 @@ export default function DetailContent() {
</div> </div>
<span className="text-[#31942E] font-medium"> <span className="text-[#31942E] font-medium">
{articleDetail?.createdByName} {articleDetail?.customCreatorName || articleDetail?.createdByName}
</span> </span>
<span></span> <span></span>
<span> <span>
<span> {new Date(articleDetail?.publishedAt ?? articleDetail?.createdAt)
{new Date(articleDetail?.createdAt).toLocaleDateString( .toLocaleString("id-ID", {
"id-ID", day: "numeric",
{ month: "long",
day: "numeric", year: "numeric",
month: "long", hour: "2-digit",
year: "numeric", minute: "2-digit",
} hour12: false,
)} timeZone: "Asia/Jakarta",
</span> })
.replace("pukul ", "")}{" "}
WIB
</span> </span>
<span></span> <span></span>
<span>{articleDetail?.categories?.[0]?.title}</span> <span>{articleDetail?.categories?.[0]?.title}</span>
</div> </div>
<div className="w-full h-auto mb-6"> <div className="w-full h-auto mb-6">
{articleDetail?.files?.[0]?.file_url ? ( {/* Gambar utama */}
<Image {detailFiles.length > 0 ? (
src={articleDetail.files[0].file_url} <>
alt="Berita" <Image
width={800} src={detailFiles[selectedIndex]?.fileUrl}
height={400} alt={detailFiles[selectedIndex]?.fileAlt || "Berita"}
className="rounded-lg w-full object-cover" width={800}
/> height={400}
className="rounded-lg w-full object-cover"
/>
<div className="flex gap-2 mt-3 overflow-x-auto">
{detailFiles.map((file, index) => (
<button
key={file.id}
onClick={() => setSelectedIndex(index)}
className={`border-2 rounded-lg ${
selectedIndex === index
? "border-red-500"
: "border-transparent"
}`}
>
<Image
src={file.fileUrl}
alt={file.fileAlt || "Thumbnail"}
width={100}
height={80}
className="object-cover"
/>
</button>
))}
</div>
</>
) : ( ) : (
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg"> <div className="h-[400px] flex items-center justify-center bg-gray-100 rounded-lg">
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p> <p className="text-gray-400">Gambar tidak tersedia</p>
</div> </div>
)} )}
{/* Slug */}
<p className="text-sm text-gray-500 mt-2 text-end"> <p className="text-sm text-gray-500 mt-2 text-end">
{articleDetail?.slug} {articleDetail?.slug}
</p> </p>
@ -336,23 +521,75 @@ export default function DetailContent() {
</Link> </Link>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<p className="text-gray-700 leading-relaxed text-justify"> <div className="prose max-w-none text-justify">
<span className="text-black font-bold text-md"> <div
Mikulnews.com - dangerouslySetInnerHTML={removeImgTags(
</span> formatTextToHtmlTag(articleDetail?.htmlDescription),
)}
className="text-sm lg:text-xl lg:leading-8 text-justify space-y-4"
/>
</div>
{articleDetail?.description} {/* <Author /> */}
</p> <div className="w-full bg-white py-6">
<Author /> <p className="mx-10 text-2xl mb-4 ">AUTHOR</p>
<div className=" border border-black p-6 flex items-center gap-6 max-w-[1200px] mx-auto">
{/* Foto Profil */}
<div className="w-20 h-20 relative ">
<Image
src="/profile.jpg"
alt="Author"
fill
className="rounded-full object-cover"
/>
</div>
{/* Info Author */}
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-800">
{articleDetail?.customCreatorName ||
articleDetail?.createdByName}
</h3>
<div className="mt-2 flex items-center gap-4 flex-wrap">
{/* Button lihat semua post */}
<button className="text-sm font-medium text-white hover:underline bg-[#655997] py-1 px-5 rounded-xl">
Lihat Semua Pos
</button>
<div className="bg-[#655997] rounded-full p-1">
<MailIcon
size={18}
className="text-white hover:text-black cursor-pointer "
></MailIcon>
</div>
<div className="bg-[#655997] rounded-full p-1">
<Link2
size={18}
className="text-white hover:text-black cursor-pointer "
></Link2>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<span className="font-semibold text-sm text-gray-700"> <span className="font-semibold text-sm text-gray-700">
Tags: Tags:
</span> </span>
<div className="flex flex-wrap gap-2 mt-1"> <div className="flex flex-wrap gap-2 mt-1">
{articleDetail?.tags ? ( {articleDetail?.tags ? (
<span className="bg-gray-100 text-gray-700 text-sm px-2 py-1 rounded"> articleDetail.tags
{articleDetail.tags} .split(",") // pisahkan berdasarkan koma
</span> .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> <span className="text-sm text-gray-500">Tidak ada tag</span>
)} )}
@ -360,16 +597,16 @@ export default function DetailContent() {
</div> </div>
</div> </div>
</div> </div>
<div className="relative mb-2 h-[120px] overflow-hidden flex items-center border my-8"> {/* <div className="relative mb-2 h-[120px] overflow-hidden flex items-center border my-8">
<Image <Image
src={"/image-kolom.png"} src={"/image-kolom.png"}
alt="Berita Utama" alt="Berita Utama"
fill fill
className="object-contain" className="object-contain"
/> />
</div> </div> */}
<div className="mt-10"> <div className="mt-10">
<div className="flex items-center space-x-4 p-4 border rounded-lg mb-6"> {/* <div className="flex items-center space-x-4 p-4 border rounded-lg mb-6">
<Image <Image
src={"/author.png"} src={"/author.png"}
alt="Author" alt="Author"
@ -382,93 +619,128 @@ export default function DetailContent() {
christine natalia christine natalia
</p> </p>
</div> </div>
</div> </div> */}
<h2 className="text-2xl font-bold mb-2">Tinggalkan Balasan</h2> <h2 className="text-2xl font-bold mb-2">Tinggalkan Balasan</h2>
<p className="text-gray-600 mb-4 text-sm"> <p className="text-gray-600 mb-4 text-sm">
Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib
ditandai <span className="text-green-600">*</span> ditandai <span className="text-green-600">*</span>
</p> </p>
<div>
<form className="space-y-6 mt-6"> <h3 className="text-lg font-semibold border-b pb-2">Komentar</h3>
<div> {commentList?.length === 0 ? (
<label <p className="text-sm text-gray-500">Belum ada komentar.</p>
htmlFor="komentar" ) : (
className="block text-sm font-medium mb-1" commentList?.map((comment: any) => (
> <div
Komentar <span className="text-green-600">*</span> key={comment?.id}
</label> className="border rounded-lg p-3 bg-gray-50 shadow-sm"
<textarea
id="komentar"
className="w-full border border-gray-300 rounded-md p-3 h-40 focus:outline-none focus:ring-2 focus:ring-green-600"
required
/>
</div>
<div>
<label
htmlFor="nama"
className="block text-sm font-medium mb-1"
>
Nama <span className="text-green-600">*</span>
</label>
<input
type="text"
id="nama"
className="w-full border border-gray-300 rounded-md p-2"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium mb-1"
> >
Email <span className="text-green-600">*</span> <p className="text-sm text-gray-800 whitespace-pre-line">
</label> {comment?.message}
<input </p>
type="email" <p className="text-xs text-gray-500 mt-1">
id="email" {comment?.commentFromName || "Anonim"} {" "}
className="w-full border border-gray-300 rounded-md p-2" {new Date(comment?.createdAt).toLocaleString("id-ID", {
required dateStyle: "short",
/> timeStyle: "short",
</div> })}
<div> </p>
<label </div>
htmlFor="website" ))
className="block text-sm font-medium mb-1" )}
</div>
<form className="space-y-6 mt-6" onSubmit={handleSubmit(onSubmit)}>
{!needOtp ? (
<>
{/* Komentar */}
<div>
<label
htmlFor="komentar"
className="block text-sm font-medium mb-1"
>
Komentar <span className="text-green-600">*</span>
</label>
<textarea
id="komentar"
className="w-full border border-gray-300 rounded-md p-3 h-40 focus:outline-none focus:ring-2 focus:ring-green-600"
{...register("comment", { required: true })}
/>
</div>
{/* Nama */}
<div>
<label
htmlFor="nama"
className="block text-sm font-medium mb-1"
>
Nama <span className="text-green-600">*</span>
</label>
<input
type="text"
id="nama"
className="w-full border border-gray-300 rounded-md p-2"
{...register("name", { required: true })}
/>
</div>
{/* Email */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium mb-1"
>
Email <span className="text-green-600">*</span>
</label>
<input
type="email"
id="email"
className="w-full border border-gray-300 rounded-md p-2"
{...register("email", { required: true })}
/>
</div>
<button
type="submit"
className="bg-green-600 hover:bg-green-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2 w-full"
> >
Situs Web KIRIM KOMENTAR
</label> </button>
<input </>
type="url" ) : (
id="website" <>
className="w-full border border-gray-300 rounded-md p-2" <p className="text-sm text-gray-600">
/> Kode verifikasi sudah dikirimkan. Silakan cek Email Anda!
</div> </p>
</div> <div>
<label className="block text-sm font-medium mb-1 mt-4">
<div className="flex items-start space-x-2 mt-2"> OTP
<input type="checkbox" id="saveInfo" className="mt-1" /> </label>
<label htmlFor="saveInfo" className="text-sm text-gray-700"> <div className="flex gap-2 justify-center">
Simpan nama, email, dan situs web saya pada peramban ini untuk {[...Array(6)].map((_, i) => (
komentar saya berikutnya. <input
</label> key={i}
</div> type="text"
maxLength={1}
<p className="text-red-600 text-sm"> className="w-10 h-10 text-center border border-gray-300 rounded-md text-lg"
The reCAPTCHA verification period has expired. Please reload the value={otpValue[i] || ""}
page. onChange={(e) => {
</p> const newValue = otpValue.split("");
newValue[i] = e.target.value.replace(/[^0-9]/g, "");
<button setOtpValue(newValue.join(""));
type="submit" }}
className="bg-green-600 hover:bg-green-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2" />
> ))}
KIRIM KOMENTAR </div>
</button> </div>
<button
type="submit"
className="bg-green-600 hover:bg-green-700 text-white font-semibold px-6 py-2 rounded-md transition mt-4 w-full"
>
Kirim
</button>
</>
)}
</form> </form>
</div> </div>
</div> </div>
@ -476,13 +748,32 @@ export default function DetailContent() {
<div className="md:col-span-1 space-y-6"> <div className="md:col-span-1 space-y-6">
<div className="sticky top-0 space-y-6"> <div className="sticky top-0 space-y-6">
<div className="bg-white shadow p-4 rounded-lg"> <div className="bg-white shadow p-4 rounded-lg">
<Image {bannerAd ? (
src={"/kolom.png"} <a
alt="Iklan" href={bannerAd.redirectLink}
width={345} target="_blank"
height={345} rel="noopener noreferrer"
className="rounded" className="block w-full"
/> >
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
<button className="mt-4 w-full bg-black text-white py-2 rounded hover:opacity-90"> <button className="mt-4 w-full bg-black text-white py-2 rounded hover:opacity-90">
Learn More Learn More
</button> </button>

View File

@ -1,7 +1,7 @@
// components/custom-editor.js import React, { useCallback, useEffect, useRef, useState } from "react";
import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react"; import { CKEditor } from "@ckeditor/ckeditor5-react";
import "@/styles/custom-editor.css";
import Editor from "@/vendor/ckeditor5/build/ckeditor"; import Editor from "@/vendor/ckeditor5/build/ckeditor";
function CustomEditor(props) { function CustomEditor(props) {
@ -47,7 +47,7 @@ function CustomEditor(props) {
padding: 1rem; padding: 1rem;
} }
p { p {
margin: 0.5em 0; margin: 0.5em 0 !important;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
margin: 1em 0 0.5em 0; 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> </div>
); );
} }

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from "react";
import { Editor } from '@tinymce/tinymce-react'; import { Editor } from "@tinymce/tinymce-react";
interface OptimizedEditorProps { interface OptimizedEditorProps {
initialData?: string; initialData?: string;
@ -9,14 +9,14 @@ interface OptimizedEditorProps {
height?: number; height?: number;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
readOnly?: boolean; readOnly?: any;
} }
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
initialData = '', initialData = "",
onChange, onChange,
height = 400, height = 400,
placeholder = 'Start typing...', placeholder = "Start typing...",
disabled = false, disabled = false,
readOnly = false, readOnly = false,
}) => { }) => {
@ -42,14 +42,30 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
height, height,
menubar: false, menubar: false,
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
], ],
toolbar: 'undo redo | blocks | ' + toolbar:
'bold italic forecolor | alignleft aligncenter ' + "undo redo | blocks | " +
'alignright alignjustify | bullist numlist outdent indent | ' + "bold italic forecolor | alignleft aligncenter " +
'removeformat | table | code | help', "alignright alignjustify | bullist numlist outdent indent | " +
"removeformat | table | code | help",
content_style: ` content_style: `
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -69,18 +85,18 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
resize: false, resize: false,
statusbar: false, statusbar: false,
// Performance optimizations // Performance optimizations
cache_suffix: '?v=1.0', cache_suffix: "?v=1.0",
browser_spellcheck: false, browser_spellcheck: false,
gecko_spellcheck: false, gecko_spellcheck: false,
// Auto-save feature // Auto-save feature
auto_save: true, auto_save: true,
auto_save_interval: '30s', auto_save_interval: "30s",
// Better mobile support // Better mobile support
mobile: { mobile: {
theme: 'silver', theme: "silver",
plugins: ['lists', 'autolink', 'link', 'image', 'table'], plugins: ["lists", "autolink", "link", "image", "table"],
toolbar: 'bold italic | bullist numlist | link image' toolbar: "bold italic | bullist numlist | link image",
} },
}} }}
/> />
); );

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from "react";
import { Editor } from '@tinymce/tinymce-react'; import { Editor } from "@tinymce/tinymce-react";
interface TinyMCEEditorProps { interface TinyMCEEditorProps {
initialData?: string; initialData?: string;
@ -11,7 +11,7 @@ interface TinyMCEEditorProps {
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
readOnly?: boolean; readOnly?: boolean;
features?: 'basic' | 'standard' | 'full'; features?: "basic" | "standard" | "full";
toolbar?: string; toolbar?: string;
language?: string; language?: string;
uploadUrl?: string; uploadUrl?: string;
@ -22,21 +22,21 @@ interface TinyMCEEditorProps {
} }
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
initialData = '', initialData = "",
onChange, onChange,
onReady, onReady,
height = 400, height = 400,
placeholder = 'Start typing...', placeholder = "Start typing...",
disabled = false, disabled = false,
readOnly = false, readOnly = false,
features = 'standard', features = "standard",
toolbar, toolbar,
language = 'en', language = "en",
uploadUrl, uploadUrl,
uploadHeaders, uploadHeaders,
className = '', className = "",
autoSave = true, autoSave = true,
autoSaveInterval = 30000 autoSaveInterval = 30000,
}) => { }) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isEditorLoaded, setIsEditorLoaded] = useState(false); const [isEditorLoaded, setIsEditorLoaded] = useState(false);
@ -47,35 +47,74 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
const getFeatureConfig = (featureLevel: string) => { const getFeatureConfig = (featureLevel: string) => {
const configs = { const configs = {
basic: { basic: {
plugins: ['lists', 'link', 'autolink', 'wordcount'], plugins: ["lists", "link", "autolink", "wordcount"],
toolbar: 'bold italic | bullist numlist | link', toolbar: "bold italic | bullist numlist | link",
menubar: false menubar: false,
}, },
standard: { standard: {
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: 'undo redo | blocks | ' + toolbar:
'bold italic forecolor | alignleft aligncenter ' + "undo redo | blocks | " +
'alignright alignjustify | bullist numlist outdent indent | ' + "bold italic forecolor | alignleft aligncenter " +
'removeformat | table | code | help', "alignright alignjustify | bullist numlist outdent indent | " +
menubar: false "removeformat | table | code | help",
menubar: false,
}, },
full: { full: {
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons', "lists",
'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking', "link",
'toc', 'imagetools', 'textpattern', 'codesample' "image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
"emoticons",
"paste",
"textcolor",
"colorpicker",
"hr",
"pagebreak",
"nonbreaking",
"toc",
"imagetools",
"textpattern",
"codesample",
], ],
toolbar: 'undo redo | formatselect | bold italic backcolor | ' + toolbar:
'alignleft aligncenter alignright alignjustify | ' + "undo redo | formatselect | bold italic backcolor | " +
'bullist numlist outdent indent | removeformat | help', "alignleft aligncenter alignright alignjustify | " +
menubar: 'file edit view insert format tools table help' "bullist numlist outdent indent | removeformat | help",
} menubar: "file edit view insert format tools table help",
},
}; };
return configs[featureLevel as keyof typeof configs] || configs.standard; return configs[featureLevel as keyof typeof configs] || configs.standard;
}; };
@ -95,7 +134,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
} }
// Set up word count tracking // Set up word count tracking
editor.on('keyup', () => { editor.on("keyup", () => {
const count = editor.plugins.wordcount.body.getCharacterCount(); const count = editor.plugins.wordcount.body.getCharacterCount();
setWordCount(count); setWordCount(count);
}); });
@ -104,24 +143,24 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
if (autoSave && !readOnly) { if (autoSave && !readOnly) {
setInterval(() => { setInterval(() => {
const content = editor.getContent(); const content = editor.getContent();
localStorage.setItem('tinymce-autosave', content); localStorage.setItem("tinymce-autosave", content);
setLastSaved(new Date()); setLastSaved(new Date());
}, autoSaveInterval); }, autoSaveInterval);
} }
// Fix cursor jumping issues // Fix cursor jumping issues
editor.on('keyup', (e: any) => { editor.on("keyup", (e: any) => {
// Prevent cursor jumping on content changes // Prevent cursor jumping on content changes
e.stopPropagation(); e.stopPropagation();
}); });
editor.on('input', (e: any) => { editor.on("input", (e: any) => {
// Prevent unnecessary re-renders // Prevent unnecessary re-renders
e.stopPropagation(); e.stopPropagation();
}); });
// Handle paste events properly // Handle paste events properly
editor.on('paste', (e: any) => { editor.on("paste", (e: any) => {
// Allow default paste behavior // Allow default paste behavior
return true; return true;
}); });
@ -130,23 +169,23 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
const handleImageUpload = (blobInfo: any, progress: any) => { const handleImageUpload = (blobInfo: any, progress: any) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!uploadUrl) { if (!uploadUrl) {
reject('No upload URL configured'); reject("No upload URL configured");
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename()); formData.append("file", blobInfo.blob(), blobInfo.filename());
fetch(uploadUrl, { fetch(uploadUrl, {
method: 'POST', method: "POST",
headers: uploadHeaders || {}, headers: uploadHeaders || {},
body: formData body: formData,
}) })
.then(response => response.json()) .then((response) => response.json())
.then(result => { .then((result) => {
resolve(result.url); resolve(result.url);
}) })
.catch(error => { .catch((error) => {
reject(error); reject(error);
}); });
}); });
@ -158,70 +197,65 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
height, height,
language, language,
placeholder, placeholder,
readonly: readOnly,
disabled,
branding: false, branding: false,
elementpath: false, elementpath: false,
resize: false, resize: false,
statusbar: !readOnly, statusbar: !readOnly,
// Performance optimizations // Performance optimizations
cache_suffix: '?v=1.0', cache_suffix: "?v=1.0",
browser_spellcheck: false, browser_spellcheck: false,
gecko_spellcheck: false, gecko_spellcheck: false,
// Content styling
content_style: ` content_style: `
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #333; color: #333;
margin: 0; margin: 0;
padding: 16px; padding: 16px;
} }
.mce-content-body { .mce-content-body {
min-height: ${height - 32}px; min-height: ${height - 32}px;
} }
.mce-content-body:focus { .mce-content-body:focus {
outline: none; outline: none;
} }
`, `,
// Image upload configuration
images_upload_handler: uploadUrl ? handleImageUpload : undefined, images_upload_handler: uploadUrl ? handleImageUpload : undefined,
automatic_uploads: !!uploadUrl, automatic_uploads: !!uploadUrl,
file_picker_types: 'image', file_picker_types: "image",
// Better mobile support
mobile: { mobile: {
theme: 'silver', theme: "silver",
plugins: ['lists', 'autolink', 'link', 'image', 'table'], plugins: ["lists", "autolink", "link", "image", "table"],
toolbar: 'bold italic | bullist numlist | link image' toolbar: "bold italic | bullist numlist | link image",
}, },
// Paste configuration
paste_as_text: false, paste_as_text: false,
paste_enable_default_filters: true, paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6', paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6",
paste_retain_style_properties: 'color background-color font-size font-weight', paste_retain_style_properties:
// Table configuration "color background-color font-size font-weight",
table_default_styles: { table_default_styles: { width: "100%" },
width: '100%' table_default_attributes: { border: "1" },
},
table_default_attributes: {
border: '1'
},
// Code configuration
codesample_languages: [ codesample_languages: [
{ text: 'HTML/XML', value: 'markup' }, { text: "HTML/XML", value: "markup" },
{ text: 'JavaScript', value: 'javascript' }, { text: "JavaScript", value: "javascript" },
{ text: 'CSS', value: 'css' }, { text: "CSS", value: "css" },
{ text: 'PHP', value: 'php' }, { text: "PHP", value: "php" },
{ text: 'Python', value: 'python' }, { text: "Python", value: "python" },
{ text: 'Java', value: 'java' }, { text: "Java", value: "java" },
{ text: 'C', value: 'c' }, { text: "C", value: "c" },
{ text: 'C++', value: 'cpp' } { text: "C++", value: "cpp" },
], ],
// ...feature config
...featureConfig, ...featureConfig,
// Custom toolbar if provided ...(toolbar && { toolbar }),
...(toolbar && { toolbar }) setup: (editor: any) => {
// ⬅️ Set readOnly di sini
editor.on("init", () => {
if (readOnly) {
editor.mode.set("readonly");
}
});
},
}; };
return ( return (
@ -240,7 +274,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between"> <div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span> <span>
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'} {autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
</span> </span>
{lastSaved && autoSave && !readOnly && ( {lastSaved && autoSave && !readOnly && (
<span> Last saved: {lastSaved.toLocaleTimeString()}</span> <span> Last saved: {lastSaved.toLocaleTimeString()}</span>
@ -255,7 +289,12 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
{/* Performance indicator */} {/* Performance indicator */}
<div className="text-xs text-gray-400 mt-1"> <div className="text-xs text-gray-400 mt-1">
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'} Bundle size:{" "}
{features === "basic"
? "~150KB"
: features === "standard"
? "~200KB"
: "~300KB"}
</div> </div>
</div> </div>
); );

View File

@ -48,7 +48,8 @@ function ViewEditor(props) {
.ckeditor-view-wrapper { .ckeditor-view-wrapper {
border-radius: 6px; border-radius: 6px;
overflow: hidden; 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); 0 1px 2px 0 rgba(0, 0, 0, 0.06);
} }

View File

@ -9,7 +9,7 @@ import { CloudUploadIcon, TimesIcon } from "@/components/icons";
import Image from "next/image"; import Image from "next/image";
import ReactSelect from "react-select"; import ReactSelect from "react-select";
import makeAnimated from "react-select/animated"; import makeAnimated from "react-select/animated";
import { htmlToString } from "@/utils/global"; import { convertDateFormatNoTime, htmlToString } from "@/utils/global";
import { close, error, loading, successToast } from "@/config/swal"; import { close, error, loading, successToast } from "@/config/swal";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
@ -44,6 +44,13 @@ import GenerateSingleArticleForm from "./generate-ai-single-form";
import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form"; import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import DatePicker from "react-datepicker";
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => { () => {
@ -82,6 +89,9 @@ const createArticleSchema = z.object({
title: z.string().min(2, { title: z.string().min(2, {
message: "Judul harus diisi", message: "Judul harus diisi",
}), }),
customCreatorName: z.string().min(2, {
message: "Judul harus diisi",
}),
slug: z.string().min(2, { slug: z.string().min(2, {
message: "Slug harus diisi", message: "Slug harus diisi",
}), }),
@ -94,6 +104,7 @@ const createArticleSchema = z.object({
tags: z.array(z.string()).nonempty({ tags: z.array(z.string()).nonempty({
message: "Minimal 1 tag", message: "Minimal 1 tag",
}), }),
source: z.enum(["internal", "external"]).optional(),
}); });
export default function CreateArticleForm() { export default function CreateArticleForm() {
@ -117,8 +128,8 @@ export default function CreateArticleForm() {
"publish" "publish"
); );
const [isScheduled, setIsScheduled] = useState(false); const [isScheduled, setIsScheduled] = useState(false);
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
const [startDateValue, setStartDateValue] = useState<any>(null); const [startTimeValue, setStartTimeValue] = useState<string>("");
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
@ -225,6 +236,8 @@ export default function CreateArticleForm() {
const request = { const request = {
id: diseData?.id, id: diseData?.id,
title: values.title, title: values.title,
customCreatorName: values.customCreatorName,
source: values.source,
articleBody: removeImgTags(values.description), articleBody: removeImgTags(values.description),
metaDescription: diseData?.metaDescription, metaDescription: diseData?.metaDescription,
metaTitle: diseData?.metaTitle, metaTitle: diseData?.metaTitle,
@ -280,6 +293,8 @@ export default function CreateArticleForm() {
title: values.title, title: values.title,
typeId: 1, typeId: 1,
slug: values.slug, slug: values.slug,
customCreatorName: values.customCreatorName,
source: values.source,
categoryIds: values.category.map((a) => a.id).join(","), categoryIds: values.category.map((a) => a.id).join(","),
tags: values.tags.join(","), tags: values.tags.join(","),
description: htmlToString(removeImgTags(values.description)), description: htmlToString(removeImgTags(values.description)),
@ -324,12 +339,34 @@ export default function CreateArticleForm() {
} }
} }
if (status === "scheduled") { if (status === "scheduled" && startDateValue) {
// ambil waktu, default 00:00 jika belum diisi
const [hours, minutes] = startTimeValue
? startTimeValue.split(":").map(Number)
: [0, 0];
// gabungkan tanggal + waktu
const combinedDate = new Date(startDateValue);
combinedDate.setHours(hours, minutes, 0, 0);
// format: 2025-10-08 14:30:00
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
combinedDate.getMonth() + 1
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
2,
"0"
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
combinedDate.getMinutes()
).padStart(2, "0")}:00`;
const request = { const request = {
id: articleId, id: articleId,
date: `${startDateValue?.year}-${startDateValue?.month}-${startDateValue?.day}`, date: formattedDateTime,
}; };
console.log("📤 Sending schedule request:", request);
const res = await createArticleSchedule(request); const res = await createArticleSchedule(request);
console.log("✅ Schedule response:", res);
} }
close(); close();
@ -524,13 +561,21 @@ export default function CreateArticleForm() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="single">Single Article</SelectItem> <SelectItem value="single">Single Article</SelectItem>
<SelectItem value="rewrite">Content Rewrite</SelectItem> {/* <SelectItem value="rewrite">Content Rewrite</SelectItem> */}
</SelectContent> </SelectContent>
</Select> </Select>
{selectedWritingType === "single" ? ( {selectedWritingType === "single" ? (
<GenerateSingleArticleForm <GenerateSingleArticleForm
content={(data) => { content={(data) => {
setDiseData(data); setDiseData(data);
// setValue("title", data?.title ?? "", {
// shouldValidate: true,
// shouldDirty: true,
// });
// setValue("slug", generateSlug(data?.title ?? ""), {
// shouldValidate: true,
// shouldDirty: true,
// });
setValue( setValue(
"description", "description",
data?.articleBody ? data?.articleBody : "" data?.articleBody ? data?.articleBody : ""
@ -651,7 +696,38 @@ export default function CreateArticleForm() {
)} )}
</> </>
)} )}
<p className="text-sm">Kreator</p>
<Controller
control={control}
name="customCreatorName"
render={({ field }) => (
<Input
id="customCreatorName"
type="text"
placeholder="Masukkan judul artikel"
className="w-full border rounded-lg dark:border-gray-400"
{...field}
/>
)}
/>
<div className="mt-2">
<p className="text-sm">Tipe Kreator</p>
<Controller
control={control}
name="source"
render={({ field }) => (
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger className="w-full border rounded-lg text-sm dark:border-gray-400">
<SelectValue placeholder="Pilih tipe kreator" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal">Internal</SelectItem>
<SelectItem value="external">External</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<p className="text-sm mt-3">Kategori</p> <p className="text-sm mt-3">Kategori</p>
<Controller <Controller
control={control} control={control}
@ -765,32 +841,49 @@ export default function CreateArticleForm() {
</label> </label>
</div> </div>
{/* {isScheduled && ( {isScheduled && (
<div className="flex flex-col lg:flex-row gap-3"> <div className="flex flex-col lg:flex-row gap-3 mt-2">
<div className="w-full lg:w-[140px] flex flex-col gal-2 "> {/* Pilih tanggal */}
<div className="w-full lg:w-[140px] flex flex-col gap-2">
<p className="text-sm">Tanggal</p> <p className="text-sm">Tanggal</p>
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
type="button" type="button"
className="w-full !h-[30px] lg:h-[40px] border-1 rounded-lg text-black" className="w-full !h-[37px] lg:h-[37px] border-1 rounded-lg text-black"
variant="outline" variant="outline"
> >
{startDateValue {startDateValue
? convertDateFormatNoTime(startDateValue) ? startDateValue.toLocaleDateString("en-CA")
: "-"} : "-"}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="bg-transparent p-0"> <PopoverContent className="bg-transparent p-0">
<Calendar <DatePicker
selected={startDateValue} selected={startDateValue}
onSelect={setStartDateValue} onChange={(date) =>
setStartDateValue(date ?? undefined)
}
dateFormat="yyyy-MM-dd"
className="w-full border rounded-lg px-2 py-1 text-black cursor-pointer h-[150px]"
placeholderText="Pilih tanggal"
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
{/* Pilih waktu */}
<div className="w-full lg:w-[140px] flex flex-col gap-2">
<p className="text-sm">Waktu</p>
<input
type="time"
value={startTimeValue}
onChange={(e) => setStartTimeValue(e.target.value)}
className="w-full border rounded-lg px-2 py-[6px] text-black"
/>
</div>
</div> </div>
)} */} )}
</div> </div>
</div> </div>

View File

@ -19,10 +19,13 @@ import GetSeoScore from "./get-seo-score-form";
import Link from "next/link"; import Link from "next/link";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { import {
createArticleSchedule,
deleteArticleFiles, deleteArticleFiles,
getArticleByCategory, getArticleByCategory,
getArticleById, getArticleById,
getArticleFiles,
submitApproval, submitApproval,
unPublishArticle,
updateArticle, updateArticle,
uploadArticleFile, uploadArticleFile,
uploadArticleThumbnail, uploadArticleThumbnail,
@ -48,6 +51,15 @@ import {
DialogTitle, DialogTitle,
DialogClose, DialogClose,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import DatePicker from "react-datepicker";
import { Switch } from "@/components/ui/switch";
const ViewEditor = dynamic( const ViewEditor = dynamic(
() => { () => {
@ -81,6 +93,9 @@ const createArticleSchema = z.object({
title: z.string().min(2, { title: z.string().min(2, {
message: "Judul harus diisi", message: "Judul harus diisi",
}), }),
customCreatorName: z.string().min(2, {
message: "Judul harus diisi",
}),
slug: z.string().min(2, { slug: z.string().min(2, {
message: "Slug harus diisi", message: "Slug harus diisi",
}), }),
@ -92,7 +107,8 @@ const createArticleSchema = z.object({
}), }),
tags: z.array(z.string()).nonempty({ tags: z.array(z.string()).nonempty({
message: "Minimal 1 tag", message: "Minimal 1 tag",
}), // Array berisi string }),
source: z.enum(["internal", "external"]).optional(),
}); });
interface DiseData { interface DiseData {
@ -136,8 +152,14 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
const [approvalStatus, setApprovalStatus] = useState<number>(2); const [approvalStatus, setApprovalStatus] = useState<number>(2);
const [approvalMessage, setApprovalMessage] = useState(""); const [approvalMessage, setApprovalMessage] = useState("");
const [detailData, setDetailData] = useState<any>(); const [detailData, setDetailData] = useState<any>();
const [startDateValue, setStartDateValue] = useState<any>(null); // const [startDateValue, setStartDateValue] = useState<any>(null);
const [timeValue, setTimeValue] = useState("00:00"); // const [timeValue, setTimeValue] = useState("00:00");
const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
"publish"
);
const [isScheduled, setIsScheduled] = useState(false);
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
const [startTimeValue, setStartTimeValue] = useState<string>("");
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
@ -175,19 +197,49 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
async function initState() { async function initState() {
loading(); loading();
const res = await getArticleById(id); try {
const data = res.data?.data; // 1⃣ Ambil ARTICLE
setDetailData(data); const articleRes = await getArticleById(id);
setValue("title", data?.title); const articleData = articleRes.data?.data;
setValue("slug", data?.slug);
setValue("description", data?.htmlDescription);
setValue("tags", data?.tags ? data.tags.split(",") : []);
setThumbnail(data?.thumbnailUrl);
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files);
setupInitCategory(data?.categories); if (!articleData) return;
close();
// ===== ARTICLE DATA =====
setDetailData(articleData);
setValue("title", articleData.title);
setValue("customCreatorName", articleData.customCreatorName);
setValue("slug", articleData.slug);
setValue("source", articleData.source);
const cleanDescription = articleData.htmlDescription
? articleData.htmlDescription
.replace(/\\"/g, '"')
.replace(/\\n/g, "\n")
.trim()
: "";
setValue("description", cleanDescription);
setValue("tags", articleData.tags ? articleData.tags.split(",") : []);
setThumbnail(articleData.thumbnailUrl);
setDiseId(articleData.aiArticleId);
setupInitCategory(articleData.categories);
// 2⃣ Ambil SEMUA article files
const filesRes = await getArticleFiles();
const allFiles = filesRes.data?.data ?? [];
// 3⃣ FILTER berdasarkan ARTICLE ID yang sedang dibuka
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === articleData.id
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state error:", error);
} finally {
close();
}
} }
const setupInitCategory = (data: any) => { const setupInitCategory = (data: any) => {
@ -242,21 +294,28 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
const doPublish = async () => { const doPublish = async () => {
MySwal.fire({ MySwal.fire({
title: "Publish Artikel?", title: isScheduled ? "Jadwalkan Publikasi?" : "Publish Artikel Sekarang?",
text: "", text: isScheduled
? "Artikel akan dipublish otomatis sesuai tanggal dan waktu yang kamu pilih."
: "",
icon: "warning", icon: "warning",
showCancelButton: true, showCancelButton: true,
cancelButtonColor: "#d33", cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
confirmButtonText: "Submit", confirmButtonText: isScheduled ? "Jadwalkan" : "Publish",
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
publish(); if (isScheduled) {
setStatus("scheduled");
publishScheduled();
} else {
publishNow();
}
} }
}); });
}; };
const publish = async () => { const publishNow = async () => {
const response = await updateArticle(String(id), { const response = await updateArticle(String(id), {
id: Number(id), id: Number(id),
isPublish: true, isPublish: true,
@ -273,8 +332,67 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
if (response?.error) { if (response?.error) {
error(response.message); error(response.message);
return false; return;
} }
successSubmit("/admin/article");
};
const publishScheduled = async () => {
if (!startDateValue) {
error("Tanggal belum dipilih!");
return;
}
const [hours, minutes] = startTimeValue
? startTimeValue.split(":").map(Number)
: [0, 0];
const combinedDate = new Date(startDateValue);
combinedDate.setHours(hours, minutes, 0, 0);
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
combinedDate.getMonth() + 1
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
2,
"0"
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
combinedDate.getMinutes()
).padStart(2, "0")}:00`;
const response = await updateArticle(String(id), {
id: Number(id),
isPublish: false,
title: detailData?.title,
typeId: 1,
slug: detailData?.slug,
categoryIds: getValues("category")
.map((val) => val.id)
.join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
const articleId = response?.data?.data?.id ?? id;
const scheduleReq = {
id: articleId,
date: formattedDateTime,
};
console.log("📅 Mengirim jadwal publish:", scheduleReq);
const res = await createArticleSchedule(scheduleReq);
if (res?.error) {
error("Gagal membuat jadwal publikasi.");
return;
}
successSubmit("/admin/article"); successSubmit("/admin/article");
}; };
@ -292,16 +410,16 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
// createdAt: `${startDateValue} ${timeValue}:00`, // createdAt: `${startDateValue} ${timeValue}:00`,
}; };
if (startDateValue && timeValue) { // if (startDateValue && timeValue) {
formData.createdAt = `${startDateValue} ${timeValue}:00`; // formData.createdAt = `${startDateValue} ${timeValue}:00`;
} // }
const response = await updateArticle(String(id), formData); const response = await updateArticle(String(id), formData);
if (response?.error) { if (response?.error) {
error(response.message); error(response.message);
return false; return false;
} }
const articleId = response?.data?.data?.id;
const formFiles = new FormData(); const formFiles = new FormData();
if (files?.length > 0) { if (files?.length > 0) {
@ -318,8 +436,38 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
const resFile = await uploadArticleThumbnail(String(id), formFiles); const resFile = await uploadArticleThumbnail(String(id), formFiles);
} }
if (status === "scheduled" && startDateValue) {
// ambil waktu, default 00:00 jika belum diisi
const [hours, minutes] = startTimeValue
? startTimeValue.split(":").map(Number)
: [0, 0];
// gabungkan tanggal + waktu
const combinedDate = new Date(startDateValue);
combinedDate.setHours(hours, minutes, 0, 0);
// format: 2025-10-08 14:30:00
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
combinedDate.getMonth() + 1
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
2,
"0"
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
combinedDate.getMinutes()
).padStart(2, "0")}:00`;
const request = {
id: articleId,
date: formattedDateTime,
};
console.log("📤 Sending schedule request:", request);
const res = await createArticleSchedule(request);
console.log("✅ Schedule response:", res);
}
close(); close();
successSubmit("/admin/article"); successSubmitData();
}; };
function successSubmit(redirect: string) { function successSubmit(redirect: string) {
@ -335,6 +483,51 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
}); });
} }
function successSubmitData() {
MySwal.fire({
title: "Berhasil disimpan!",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
});
}
const doUnpublish = async () => {
MySwal.fire({
title: "Unpublish Artikel?",
text: "Artikel akan dihapus dari publik dan tidak tampil lagi.",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Ya, Unpublish",
}).then(async (result) => {
if (result.isConfirmed) {
loading();
const response = await unPublishArticle(String(id), {
id: Number(id),
isPublish: false,
title: detailData?.title,
typeId: 1,
slug: detailData?.slug,
categoryIds: getValues("category")
.map((val) => val.id)
.join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
successSubmit("/admin/article");
}
});
};
const watchTitle = watch("title"); const watchTitle = watch("title");
const generateSlug = (title: string) => { const generateSlug = (title: string) => {
return title return title
@ -497,9 +690,10 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
name="title" name="title"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<div className="w-full"> <div className="w-full">
<label htmlFor="title" className="block text-sm font-medium mb-1"> <label htmlFor="title" className="block text-xl font-medium mb-2">
Judul Judul
</label> </label>
<Input <Input
type="text" type="text"
id="title" id="title"
@ -507,7 +701,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
value={value ?? ""} value={value ?? ""}
readOnly={isDetail} readOnly={isDetail}
onChange={onChange} onChange={onChange}
className="w-full border rounded-lg" className="h-16 px-4 text-2xl leading-tight"
/> />
</div> </div>
)} )}
@ -623,9 +817,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
alt="main" alt="main"
width={720} width={720}
height={480} height={480}
src={ src={detailfiles[mainImage]?.fileUrl || "/default-avatar.png"}
detailfiles[mainImage]?.file_url || "/default-avatar.png"
}
className="w-[75%] mx-auto" className="w-[75%] mx-auto"
/> />
</div> </div>
@ -640,7 +832,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
width={480} width={480}
height={360} height={360}
alt={`image-${index}`} alt={`image-${index}`}
src={file.file_url || "/default-avatar.png"} src={file.fileUrl || "/default-avatar.png"}
className="h-[100px] object-cover w-[150px]" className="h-[100px] object-cover w-[150px]"
/> />
</a> </a>
@ -663,7 +855,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
width={480} width={480}
height={360} height={360}
alt={`image-${index}`} alt={`image-${index}`}
src={file?.file_url || "/default-avatar.png"} src={file?.fileUrl || "/default-avatar.png"}
className="h-[100px] object-cover w-[150px]" className="h-[100px] object-cover w-[150px]"
/> />
</div> </div>
@ -683,6 +875,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
</div> </div>
<Button <Button
type="button"
className=" border-none rounded-full" className=" border-none rounded-full"
variant="outline" variant="outline"
color="danger" color="danger"
@ -784,6 +977,43 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
)} )}
</> </>
)} )}
<p className="text-sm">Kreator</p>
<Controller
control={control}
name="customCreatorName"
render={({ field }) => (
<Input
id="customCreatorName"
type="text"
placeholder="Masukkan Kreator artikel"
readOnly={isDetail}
className="w-full border rounded-lg dark:border-gray-400"
{...field}
/>
)}
/>
<div className="mt-2">
<p className="text-sm">Tipe Kreator</p>
<Controller
control={control}
name="source"
render={({ field }) => (
<Select
onValueChange={field.onChange}
value={field.value}
disabled={isDetail}
>
<SelectTrigger className="w-full border rounded-lg text-sm dark:border-gray-400">
<SelectValue placeholder="Pilih tipe kreator" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal">Internal</SelectItem>
<SelectItem value="external">External</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<p className="text-sm mt-3">Kategori</p> <p className="text-sm mt-3">Kategori</p>
<Controller <Controller
control={control} control={control}
@ -885,7 +1115,63 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
{errors?.tags && ( {errors?.tags && (
<p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p> <p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p>
)} )}
{!isDetail && username === "admin-mabes" && ( <div className="flex flex-col gap-2 mt-3">
<div className="flex items-center space-x-2">
<Switch
id="schedule-switch"
checked={isScheduled}
onCheckedChange={setIsScheduled}
/>
<label htmlFor="schedule-switch" className="text-black text-sm">
Publish dengan Jadwal
</label>
</div>
{isScheduled && (
<div className="flex flex-col lg:flex-row gap-3 mt-2">
{/* Pilih tanggal */}
<div className="w-full lg:w-[140px] flex flex-col gap-2">
<p className="text-sm">Tanggal</p>
<Popover>
<PopoverTrigger>
<Button
type="button"
className="w-full !h-[37px] lg:h-[37px] border-1 rounded-lg text-black"
variant="outline"
>
{startDateValue
? startDateValue.toLocaleDateString("en-CA")
: "-"}
</Button>
</PopoverTrigger>
<PopoverContent className="bg-transparent p-0">
<DatePicker
selected={startDateValue}
onChange={(date) =>
setStartDateValue(date ?? undefined)
}
dateFormat="yyyy-MM-dd"
className="w-full border rounded-lg px-2 py-1 text-black cursor-pointer h-[150px]"
placeholderText="Pilih tanggal"
/>
</PopoverContent>
</Popover>
</div>
{/* Pilih waktu */}
<div className="w-full lg:w-[140px] flex flex-col gap-2">
<p className="text-sm">Waktu</p>
<input
type="time"
value={startTimeValue}
onChange={(e) => setStartTimeValue(e.target.value)}
className="w-full border rounded-lg px-2 py-[6px] text-black"
/>
</div>
</div>
)}
</div>
{/* {!isDetail && username === "admin-mabes" && (
<> <>
<p className="text-sm">Ubah Waktu Pembuatan</p> <p className="text-sm">Ubah Waktu Pembuatan</p>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
@ -917,7 +1203,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
/> />
</div> </div>
</> </>
)} )} */}
</div> </div>
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">
@ -955,8 +1241,13 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
</Button> </Button>
)} )}
{detailData?.isPublish === false && ( {detailData?.isPublish === false && (
<Button type="button" color="primary" onClick={doPublish}> <Button
Publish type="button"
color="primary"
onClick={doPublish}
disabled={isScheduled && !startDateValue}
>
{isScheduled ? "Jadwalkan" : "Publish"}
</Button> </Button>
)} )}
{/* {!isDetail && ( {/* {!isDetail && (
@ -965,6 +1256,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
</Button> </Button>
)} */} )} */}
{detailData?.isPublish == true && (
<Button
className="bg-red-500 text-white"
variant="outline"
type="button"
onClick={doUnpublish}
>
Unpublish
</Button>
)}
<Link href="/admin/article"> <Link href="/admin/article">
<Button color="danger" variant="outline" type="button"> <Button color="danger" variant="outline" type="button">
Kembali Kembali

View File

@ -1,11 +1,20 @@
"use client"; "use client";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { close, error, loading } from "@/config/swal"; import { close, error, loading } from "@/config/swal";
import { delay } from "@/utils/global"; import { delay } from "@/utils/global";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getDetailArticle, getGenerateRewriter } from "@/service/generate-article"; import {
getDetailArticle,
getGenerateRewriter,
} from "@/service/generate-article";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import GetSeoScore from "./get-seo-score-form"; import GetSeoScore from "./get-seo-score-form";
@ -69,8 +78,11 @@ interface DiseData {
additionalKeywords: string; additionalKeywords: string;
} }
export default function GenerateContentRewriteForm(props: { content: (data: DiseData) => void }) { export default function GenerateContentRewriteForm(props: {
const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational"); content: (data: DiseData) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");
const [selectedArticleSize, setSelectedArticleSize] = useState("News"); const [selectedArticleSize, setSelectedArticleSize] = useState("News");
const [selectedLanguage, setSelectedLanguage] = useState("id"); const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState(""); const [mainKeyword, setMainKeyword] = useState("");
@ -166,7 +178,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
))} ))}
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedWritingSyle} onValueChange={(value) => setSelectedWritingStyle(value)}> <Select
value={selectedWritingSyle}
onValueChange={(value) => setSelectedWritingStyle(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -198,7 +213,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
))} ))}
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedArticleSize} onValueChange={(value) => setSelectedArticleSize(value)}> <Select
value={selectedArticleSize}
onValueChange={(value) => setSelectedArticleSize(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -229,7 +247,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
<SelectItem key="en">English</SelectItem> <SelectItem key="en">English</SelectItem>
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)}> <Select
value={selectedLanguage}
onValueChange={(value) => setSelectedLanguage(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -239,6 +260,7 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex flex-col mt-3"> <div className="flex flex-col mt-3">
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<p className="text-sm">Text</p> <p className="text-sm">Text</p>
@ -246,9 +268,16 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
<div className="w-[78vw] lg:w-full"> <div className="w-[78vw] lg:w-full">
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} /> <CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
</div> </div>
{mainKeyword == "" && <p className="text-red-400 text-sm">Required</p>} {mainKeyword == "" && (
<p className="text-red-400 text-sm">Required</p>
)}
{articleIds.length < 3 && ( {articleIds.length < 3 && (
<Button onClick={onSubmit} type="button" disabled={mainKeyword === "" || isLoading} className="my-5 w-full py-5 text-xs md:text-base"> <Button
onClick={onSubmit}
type="button"
disabled={mainKeyword === "" || isLoading}
className="my-5 w-full py-5 text-xs md:text-base"
>
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -263,7 +292,14 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
{articleIds.length > 0 && ( {articleIds.length > 0 && (
<div className="flex flex-row gap-1 mt-2"> <div className="flex flex-row gap-1 mt-2">
{articleIds?.map((id, index) => ( {articleIds?.map((id, index) => (
<Button key={id} onClick={() => setSelectedId(id)} disabled={isLoading && selectedId === id} variant={selectedId === id ? "default" : "outline"} className="flex items-center gap-2"> <Button
type="button"
key={id}
onClick={() => setSelectedId(id)}
disabled={isLoading && selectedId === id}
variant={selectedId === id ? "default" : "outline"}
className="flex items-center gap-2"
>
{isLoading && selectedId === id ? ( {isLoading && selectedId === id ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />

View File

@ -76,16 +76,15 @@ interface DiseData {
export default function GenerateSingleArticleForm(props: { export default function GenerateSingleArticleForm(props: {
content: (data: DiseData) => void; content: (data: DiseData) => void;
}) { }) {
const [selectedWritingSyle, setSelectedWritingStyle] = const [selectedWritingSyle, setSelectedWritingStyle] = useState("");
useState("Informational"); const [selectedArticleSize, setSelectedArticleSize] = useState("");
const [selectedArticleSize, setSelectedArticleSize] = useState("News"); const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState(""); const [mainKeyword, setMainKeyword] = useState("");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [additionalKeyword, setAdditionalKeyword] = useState(""); const [additionalKeyword, setAdditionalKeyword] = useState("");
const [articleIds, setArticleIds] = useState<number[]>([]); const [articleIds, setArticleIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number>(); const [selectedId, setSelectedId] = useState<number>();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(false);
const generateAll = async (keyword: string | undefined) => { const generateAll = async (keyword: string | undefined) => {
if (keyword) { if (keyword) {
@ -271,11 +270,11 @@ export default function GenerateSingleArticleForm(props: {
}} }}
> >
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black"> <SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Article Size" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{articleSize.map((style) => ( {articleSize.map((style) => (
<SelectItem key={style.name} value={style.name}> <SelectItem key={style.name} value={style.value}>
{style.name} {style.name}
</SelectItem> </SelectItem>
))} ))}
@ -307,7 +306,7 @@ export default function GenerateSingleArticleForm(props: {
}} }}
> >
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black"> <SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
<SelectValue placeholder="Bahasa" /> <SelectValue placeholder="Language" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="id">Indonesia</SelectItem> <SelectItem value="id">Indonesia</SelectItem>
@ -319,6 +318,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<p className="text-sm">Main Keyword</p> <p className="text-sm">Main Keyword</p>
<Button <Button
type="button"
variant="default" variant="default"
size="sm" size="sm"
onClick={() => generateAll(mainKeyword)} onClick={() => generateAll(mainKeyword)}
@ -350,6 +350,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center mt-3"> <div className="flex flex-row gap-2 items-center mt-3">
<p className="text-sm">Title</p> <p className="text-sm">Title</p>
<Button <Button
type="button"
variant="default" variant="default"
size="sm" size="sm"
onClick={() => generateTitle(mainKeyword)} onClick={() => generateTitle(mainKeyword)}
@ -373,6 +374,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center mt-2"> <div className="flex flex-row gap-2 items-center mt-2">
<p className="text-sm">Additional Keyword</p> <p className="text-sm">Additional Keyword</p>
<Button <Button
type="button"
className="text-sm" className="text-sm"
size="sm" size="sm"
onClick={() => generateKeywords(mainKeyword)} onClick={() => generateKeywords(mainKeyword)}
@ -417,6 +419,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-1 mt-2"> <div className="flex flex-row gap-1 mt-2">
{articleIds.map((id, index) => ( {articleIds.map((id, index) => (
<Button <Button
type="button"
key={id} key={id}
onClick={() => setSelectedId(id)} onClick={() => setSelectedId(id)}
disabled={isLoading && selectedId === id} disabled={isLoading && selectedId === id}

View File

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import news from "../news"; import news from "../news";
import Link from "next/link"; import Link from "next/link";
import { getAdvertise } from "@/service/advertisement";
type Article = { type Article = {
id: number; id: number;
@ -12,13 +13,15 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -32,6 +35,15 @@ const slugToLabel = (slug: string) => {
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1); return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
}; };
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
export default function CitizenNews() { export default function CitizenNews() {
const [activeTab, setActiveTab] = useState("comments"); const [activeTab, setActiveTab] = useState("comments");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -51,6 +63,36 @@ export default function CitizenNews() {
const categorySlug = pathSegments[1]; const categorySlug = pathSegments[1];
const categoryLabel = slugToLabel(categorySlug); const categoryLabel = slugToLabel(categorySlug);
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
useEffect(() => {
initStateAdver();
}, []);
async function initStateAdver() {
const req = {
limit: 100,
page: 1,
sort: "desc",
sortBy: "created_at",
isPublish: true,
};
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(() => { useEffect(() => {
initState(); initState();
}, [page, showData, startDateValue, selectedCategories, activeTab]); }, [page, showData, startDateValue, selectedCategories, activeTab]);
@ -81,8 +123,8 @@ export default function CitizenNews() {
} }
const citizenArticles = articles.filter((article) => const citizenArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "berita warga" category.title?.toLowerCase().includes("berita warga")
) )
); );
@ -109,7 +151,7 @@ export default function CitizenNews() {
<div key={item.id}> <div key={item.id}>
<Link <Link
className="flex flex-col md:flex-row gap-6" className="flex flex-col md:flex-row gap-6"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
{/* Image + Category */} {/* Image + Category */}
<div className="relative w-full md:w-1/2 h-64"> <div className="relative w-full md:w-1/2 h-64">
@ -132,7 +174,7 @@ export default function CitizenNews() {
<div className="text-sm text-gray-600 mt-2"> <div className="text-sm text-gray-600 mt-2">
BY{" "} BY{" "}
<span className="text-green-600 font-semibold"> <span className="text-green-600 font-semibold">
{item.createdByName || "Admin"} {item?.customCreatorName || item.createdByName}
</span>{" "} </span>{" "}
{new Date(item.createdAt).toLocaleDateString("id-ID")} {new Date(item.createdAt).toLocaleDateString("id-ID")}
</div> </div>
@ -204,12 +246,32 @@ export default function CitizenNews() {
<div className="space-y-6"> <div className="space-y-6">
{/* Advertisement */} {/* Advertisement */}
<div className="w-full h-[400px] relative"> <div className="w-full h-[400px] relative">
<Image {bannerAd ? (
src="/advertisiment.png" <a
alt="Advertisement" href={bannerAd.redirectLink}
fill target="_blank"
className="object-cover rounded" rel="noopener noreferrer"
/> className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div> </div>
{/* Connect with us */} {/* Connect with us */}
@ -258,7 +320,7 @@ export default function CitizenNews() {
<div key={item.id} className="flex gap-3 items-center"> <div key={item.id} className="flex gap-3 items-center">
<Link <Link
className="flex gap-3 items-center" className="flex gap-3 items-center"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
<Image <Image
src={item.thumbnailUrl || "/no-image.jpg"} src={item.thumbnailUrl || "/no-image.jpg"}
@ -283,11 +345,11 @@ export default function CitizenNews() {
</h2> </h2>
<div className=" w-full"> <div className=" w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={ src={
articles[0]?.thumbnailUrl || articles[0]?.thumbnailUrl ||
articles[0]?.files?.[0]?.file_url || articles[0]?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
@ -333,13 +395,13 @@ export default function CitizenNews() {
<div key={index}> <div key={index}>
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
src={ src={
article?.thumbnailUrl || article?.thumbnailUrl ||
article?.files?.[0]?.file_url || article?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"article?.title"} alt={"article?.title"}

View File

@ -12,13 +12,15 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -76,8 +78,8 @@ export default function HeaderCitizen() {
} }
const citizenArticles = articles.filter((article) => const citizenArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "berita warga" category.title?.toLowerCase().includes("berita warga")
) )
); );
@ -108,9 +110,9 @@ export default function HeaderCitizen() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
{mainArticle && ( {mainArticle && (
<div className="md:col-span-2 relative"> <div className="md:col-span-2 relative">
<Link href={`/detail/${mainArticle.id}`}> <Link href={`/details/${mainArticle.slug}`}>
<Image <Image
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"} src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
alt={mainArticle.title} alt={mainArticle.title}
width={800} width={800}
height={500} height={500}
@ -124,7 +126,9 @@ export default function HeaderCitizen() {
{mainArticle.title} {mainArticle.title}
</h2> </h2>
<p className="text-white text-xs"> <p className="text-white text-xs">
{mainArticle.createdByName} -{" "} {mainArticle?.customCreatorName ||
mainArticle.createdByName}{" "}
-{" "}
{new Date(mainArticle.createdAt).toLocaleDateString( {new Date(mainArticle.createdAt).toLocaleDateString(
"id-ID", "id-ID",
{ {
@ -142,11 +146,11 @@ export default function HeaderCitizen() {
<div className="grid grid-rows-2 gap-2"> <div className="grid grid-rows-2 gap-2">
{otherArticles.map((article, index) => ( {otherArticles.map((article, index) => (
<div key={index} className="relative"> <div key={index} className="relative">
<Link href={`/detail/${article.id}`}> <Link href={`/details/${article.slug}`}>
<Image <Image
src={ src={
article.thumbnailUrl || article.thumbnailUrl ||
article.files?.[0]?.file_url || article.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={article.title} alt={article.title}

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]">
BERITA OPINI
</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

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import news from "../news"; import news from "../news";
import Link from "next/link"; import Link from "next/link";
import { getAdvertise } from "@/service/advertisement";
type Article = { type Article = {
id: number; id: number;
@ -13,16 +14,27 @@ type Article = {
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
createdByName: string; createdByName: string;
slug: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
const slugToLabel = (slug: string) => { const slugToLabel = (slug: string) => {
const mapping: Record<string, string> = { const mapping: Record<string, string> = {
development: "Pembangunan", development: "Pembangunan",
@ -48,6 +60,36 @@ export default function DevelopmentNews() {
const pathname = usePathname(); const pathname = usePathname();
const pathSegments = pathname.split("/").filter(Boolean); const pathSegments = pathname.split("/").filter(Boolean);
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
useEffect(() => {
initStateAdver();
}, []);
async function initStateAdver() {
const req = {
limit: 100,
page: 1,
sort: "desc",
sortBy: "created_at",
isPublish: true,
};
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);
}
}
const categorySlug = pathSegments[1]; const categorySlug = pathSegments[1];
const categoryLabel = slugToLabel(categorySlug); const categoryLabel = slugToLabel(categorySlug);
@ -81,8 +123,8 @@ export default function DevelopmentNews() {
} }
const pembangunanArticles = articles.filter((article) => const pembangunanArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "pembangunan" category.title?.toLowerCase().includes("berita warga")
) )
); );
@ -111,7 +153,7 @@ export default function DevelopmentNews() {
<div key={item.id}> <div key={item.id}>
<Link <Link
className="flex flex-col md:flex-row gap-6" className="flex flex-col md:flex-row gap-6"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
{/* Image + Category */} {/* Image + Category */}
<div className="relative w-full md:w-1/2 h-64"> <div className="relative w-full md:w-1/2 h-64">
@ -134,7 +176,7 @@ export default function DevelopmentNews() {
<div className="text-sm text-gray-600 mt-2"> <div className="text-sm text-gray-600 mt-2">
BY{" "} BY{" "}
<span className="text-green-600 font-semibold"> <span className="text-green-600 font-semibold">
{item.createdByName || "Admin"} {item?.customCreatorName || item.createdByName}
</span>{" "} </span>{" "}
{new Date(item.createdAt).toLocaleDateString("id-ID")} {new Date(item.createdAt).toLocaleDateString("id-ID")}
</div> </div>
@ -203,12 +245,32 @@ export default function DevelopmentNews() {
<div className="space-y-6"> <div className="space-y-6">
{/* Advertisement */} {/* Advertisement */}
<div className="w-full h-[400px] relative"> <div className="w-full h-[400px] relative">
<Image {bannerAd ? (
src="/advertisiment.png" <a
alt="Advertisement" href={bannerAd.redirectLink}
fill target="_blank"
className="object-cover rounded" rel="noopener noreferrer"
/> className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div> </div>
{/* Connect with us */} {/* Connect with us */}
@ -257,7 +319,7 @@ export default function DevelopmentNews() {
<div key={item.id} className="flex gap-3 items-center"> <div key={item.id} className="flex gap-3 items-center">
<Link <Link
className="flex gap-3 items-center" className="flex gap-3 items-center"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
<Image <Image
src={item.thumbnailUrl || "/no-image.jpg"} src={item.thumbnailUrl || "/no-image.jpg"}
@ -282,11 +344,11 @@ export default function DevelopmentNews() {
</h2> </h2>
<div className=" w-full"> <div className=" w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={ src={
articles[0]?.thumbnailUrl || articles[0]?.thumbnailUrl ||
articles[0]?.files?.[0]?.file_url || articles[0]?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
@ -332,13 +394,13 @@ export default function DevelopmentNews() {
<div key={index}> <div key={index}>
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
src={ src={
article?.thumbnailUrl || article?.thumbnailUrl ||
article?.files?.[0]?.file_url || article?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"article?.title"} alt={"article?.title"}

View File

@ -12,13 +12,15 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -70,19 +72,21 @@ export default function HeaderDevelopment() {
const res = await getListArticle(req); const res = await getListArticle(req);
setArticles(res?.data?.data || []); setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1); setTotalPage(res?.data?.meta?.totalPage || 1);
console.log("develo", res?.data?.data || []);
} finally { } finally {
// close(); // close();
} }
} }
const pembangunanArticles = articles.filter((article) => const pembangunanArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "pembangunan" category.title?.toLowerCase().includes("berita warga")
) )
); );
const mainArticle = pembangunanArticles[0]; const mainArticle = pembangunanArticles[0];
const otherArticles = pembangunanArticles.slice(1, 3); const otherArticles = pembangunanArticles.slice(1, 3);
console.log("otherArticles:", otherArticles);
return ( return (
<section className="max-w-7xl mx-auto bg-white"> <section className="max-w-7xl mx-auto bg-white">
@ -112,9 +116,9 @@ export default function HeaderDevelopment() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
{mainArticle && ( {mainArticle && (
<div className="md:col-span-2 relative"> <div className="md:col-span-2 relative">
<Link href={`/detail/${mainArticle.id}`}> <Link href={`/details/${mainArticle.slug}`}>
<Image <Image
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"} src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
alt={mainArticle.title} alt={mainArticle.title}
width={800} width={800}
height={500} height={500}
@ -128,7 +132,9 @@ export default function HeaderDevelopment() {
{mainArticle.title} {mainArticle.title}
</h2> </h2>
<p className="text-white text-xs"> <p className="text-white text-xs">
{mainArticle.createdByName} -{" "} {mainArticle?.customCreatorName ||
mainArticle.createdByName}{" "}
-{" "}
{new Date(mainArticle.createdAt).toLocaleDateString( {new Date(mainArticle.createdAt).toLocaleDateString(
"id-ID", "id-ID",
{ {
@ -146,11 +152,11 @@ export default function HeaderDevelopment() {
<div className="grid grid-rows-2 gap-2"> <div className="grid grid-rows-2 gap-2">
{otherArticles.map((article, index) => ( {otherArticles.map((article, index) => (
<div key={index} className="relative"> <div key={index} className="relative">
<Link href={`/detail/${article.id}`}> <Link href={`/details/${article.slug}`}>
<Image <Image
src={ src={
article.thumbnailUrl || article.thumbnailUrl ||
article.files?.[0]?.file_url || article.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={article.title} alt={article.title}

View File

@ -1,176 +1,66 @@
// components/Footer.tsx // components/Footer.tsx
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import { Facebook, Twitter, Instagram, Youtube } from "lucide-react";
export default function Footer() { export default function Footer() {
return ( return (
<footer className=" text-[#FFFFFFCC] text-sm font-sans "> <footer className="bg-[#ECEFF5] pt-20 pb-10 w-full">
<div className="max-w-7xl mx-auto py-10 bg-[#09282C] px-8"> <div className="max-w-screen-xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 ">
{/* Top Menu Links */} {/* Logo */}
<div className="flex flex-col md:flex-row justify-center md:justify-between gap-3"> <div className="flex justify-center md:justify-end">
{/* <div className="w-full md:w-2/12"> <Image
<p className="text-sm text-gray-400 mt-5"> src="/mikul-news-logo.png"
© 2025{" "} alt="Logo"
<span className="text-xs text-white font-semibold">JNews</span>- width={230}
Premium WordPress news & magazine theme by{" "} height={230}
<span className="text-white font-semibold">Jegtheme</span> className="object-contain"
</p> />
</div> */} </div>
<div className="flex items-center overflow-hidden mb-4 py-6 px-8">
<Image {/* Subscribe Box */}
src="/mikul.png" <div className="flex justify-center md:justify-end">
alt="Background" <div className=" p-8 w-full md:w-[420px]">
width={272} <h2 className="text-2xl font-semibold text-gray-800 leading-snug">
height={90} Subscribe us to get <br />
className="w-full md:w-[272px] h-[90px] object-cover border" the latest news!
priority </h2>
<label className="block mt-6 mb-1 text-sm text-gray-600">
Email address:
</label>
<input
type="email"
placeholder="Your email address"
className="w-full border border-gray-300 rounded-md px-4 py-3 outline-none"
/> />
</div>
<div className="w-full md:w-6/12">
<h2 className="border-b-2 mb-5"></h2>
<div className="flex items-start flex-wrap justify-start md:justify-start gap-2 md:gap-3 text-xs text-white font-semibold">
{[
{ label: "Beranda", href: "#" },
{ label: "Pembangunan", href: "/category/development" },
{ label: "Kesehatan", href: "/category/health" },
{ label: "Berita Warga", href: "/category/citizen-news" },
].map((item, idx, arr) => (
<span
key={idx}
className="flex items-center gap-2 whitespace-nowrap"
>
<a href={item.href} className="hover:underline">
{item.label}
</a>
{idx !== arr.length - 1 && (
<span className="text-white">/</span>
)}
</span>
))}
</div>
</div>
<div className=" w-full md:w-3/12"> <button className="mt-4 bg-green-600 hover:bg-green-500 text-black px-6 py-3 rounded-md font-medium">
<div className="flex flex-col justify-center md:justify-end gap-2 md:gap-3 text-xs text-[#FFFFFF]"> SIGN UP
<p className="text-xs font-bold text-red-600 mb-2 md:mb-0 w-10/12 text-start"> </button>
Follow Us
</p>
<h2 className="border-b-2 "></h2>
<div className="flex gap-6 text-white text-lg">
<Link href="#" aria-label="Facebook">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
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>
<Link href="#" aria-label="Twitter">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
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>
<Link href="#" aria-label="Google" className="text-[#F5F5F5]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M7.796 14.333v-2.618h7.211c.066.382.12.763.12 1.265c0 4.364-2.923 7.462-7.33 7.462A7.63 7.63 0 0 1 .16 12.806a7.63 7.63 0 0 1 7.636-7.637c2.062 0 3.786.753 5.117 1.997L10.84 9.162c-.567-.546-1.56-1.178-3.044-1.178c-2.607 0-4.734 2.16-4.734 4.822s2.127 4.821 4.734 4.821c3.022 0 4.157-2.17 4.331-3.294zm13.27-2.6H23.2v2.134h-2.133V16h-2.134v-2.133H16.8v-2.134h2.133V9.6h2.134z"
// clip-rule="evenodd"
/>
</svg>
</Link>
<Link href="#" aria-label="Pinterest">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<defs>
<path
id="akarIconsPinterestFill0"
fill="#fff"
d="M0 0h24v24H0z"
/>
</defs>
<g fill="none">
<g
// clip-path="url(#akarIconsPinterestFill1)"
>
<g
// clip-path="url(#akarIconsPinterestFill2)"
>
<path
fill="currentColor"
d="M0 12c0 5.123 3.211 9.497 7.73 11.218c-.11-.937-.227-2.482.025-3.566c.217-.932 1.401-5.938 1.401-5.938s-.357-.715-.357-1.774c0-1.66.962-2.9 2.161-2.9c1.02 0 1.512.765 1.512 1.682c0 1.025-.653 2.557-.99 3.978c-.281 1.189.597 2.159 1.769 2.159c2.123 0 3.756-2.239 3.756-5.471c0-2.861-2.056-4.86-4.991-4.86c-3.398 0-5.393 2.549-5.393 5.184c0 1.027.395 2.127.889 2.726a.36.36 0 0 1 .083.343c-.091.378-.293 1.189-.332 1.355c-.053.218-.173.265-.4.159c-1.492-.694-2.424-2.875-2.424-4.627c0-3.769 2.737-7.229 7.892-7.229c4.144 0 7.365 2.953 7.365 6.899c0 4.117-2.595 7.431-6.199 7.431c-1.211 0-2.348-.63-2.738-1.373c0 0-.599 2.282-.744 2.84c-.282 1.084-1.064 2.456-1.549 3.235C9.584 23.815 10.77 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0S0 5.373 0 12"
/>
</g>
</g>
<defs>
<clipPath id="akarIconsPinterestFill1">
<use href="#akarIconsPinterestFill0" />
</clipPath>
<clipPath id="akarIconsPinterestFill2">
<use href="#akarIconsPinterestFill0" />
</clipPath>
</defs>
</g>
</svg>
</Link>
<Link href="#" aria-label="Vk">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M23.45 5.948c.166-.546 0-.948-.795-.948H20.03c-.668 0-.976.347-1.143.73c0 0-1.335 3.196-3.226 5.272c-.612.602-.89.793-1.224.793c-.167 0-.418-.191-.418-.738V5.948c0-.656-.184-.948-.74-.948H9.151c-.417 0-.668.304-.668.593c0 .621.946.765 1.043 2.513v3.798c0 .833-.153.984-.487.984c-.89 0-3.055-3.211-4.34-6.885C4.45 5.288 4.198 5 3.527 5H.9c-.75 0-.9.347-.9.73c0 .682.89 4.07 4.145 8.551C6.315 17.341 9.37 19 12.153 19c1.669 0 1.875-.368 1.875-1.003v-2.313c0-.737.158-.884.687-.884c.39 0 1.057.192 2.615 1.667C19.11 18.216 19.403 19 20.405 19h2.625c.75 0 1.126-.368.91-1.096c-.238-.724-1.088-1.775-2.215-3.022c-.612-.71-1.53-1.475-1.809-1.858c-.389-.491-.278-.71 0-1.147c0 0 3.2-4.426 3.533-5.929"
// clip-rule="evenodd"
/>
</svg>
</Link>
<Link href="#" aria-label="Wifi">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 21q-1.05 0-1.775-.725T9.5 18.5t.725-1.775T12 16t1.775.725t.725 1.775t-.725 1.775T12 21m-5.65-5.65l-2.1-2.15q1.475-1.475 3.463-2.337T12 10t4.288.875t3.462 2.375l-2.1 2.1q-1.1-1.1-2.55-1.725T12 13t-3.1.625t-2.55 1.725M2.1 11.1L0 9q2.3-2.35 5.375-3.675T12 4t6.625 1.325T24 9l-2.1 2.1q-1.925-1.925-4.462-3.012T12 7T6.563 8.088T2.1 11.1"
/>
</svg>
</Link>
</div>
</div>
</div> </div>
</div> </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 Mikul News - All Rights Reserved.
</p>
</footer> </footer>
); );
} }

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import { getListArticle } from "@/service/article"; import { getListArticle } from "@/service/article";
import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react";
type Article = { type Article = {
id: number; id: number;
@ -11,157 +11,221 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
publishedAt: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: { title: string }[];
title: string; files: { fileUrl: string; file_alt: string }[];
}[];
files: {
file_url: string;
file_alt: string;
}[];
}; };
export default function HeroNewsSection() { export default function Header() {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]); const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("5");
const [search, setSearch] = useState("");
const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
useEffect(() => { useEffect(() => {
initState(); const fetchArticles = async () => {
}, [page, showData, startDateValue, selectedCategories]); const req = {
limit: "10",
page: 1,
search: "",
categorySlug: "",
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
async function initState() {
// loading();
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req); const res = await getListArticle(req);
setArticles(res?.data?.data || []); setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1); };
} finally {
// close(); fetchArticles();
} }, []);
}
const flashArticles = articles.slice(0, 8);
const mainArticle = articles[8] || articles[0];
const recentPosts = articles.slice(1, 5);
return ( return (
<section className="max-w-7xl mx-auto bg-white"> <section className="max-w-7xl mx-auto bg-white px-4">
<div className="flex items-center bg-[#F2F4F3] w-full overflow-hidden mb-4 py-6 px-8"> {/* FLASH STRIP */}
<Image <div className="flex items-center justify-between mt-6 mb-3">
src="/mikul.png" <div className="flex items-center gap-3">
alt="Background" <h4 className="text-green-600 font-semibold">Flash</h4>
width={272} <span className="text-red-500"></span>
height={90} </div>
className="w-full md:w-[272px] h-[90px] object-cover border" <div className="text-xs text-gray-500 hidden md:block">LOAD MORE </div>
priority
/>
</div> </div>
<div className="pb-5"> <div className="overflow-x-auto no-scrollbar py-2">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-2 m-8 "> <div className="flex gap-4">
{articles.length > 0 && ( {flashArticles.map((item) => (
<div className="md:col-span-2 lg:col-span-3 relative"> <Link
<Link href={`/detail/${articles[0]?.id}`}> 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 <Image
src={ src={
articles[0]?.files?.[0]?.file_url || item.thumbnailUrl ||
articles[0]?.files?.[0]?.file_url || item.files?.[0]?.fileUrl ||
"/default-image.jpg" "/placeholder.jpg"
} }
alt={articles[0].title} alt={item.title}
width={800} fill
height={500} className="object-cover"
className="w-full h-full max-h-[460px] object-cover"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent p-6 flex flex-col justify-end"> </div>
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"} {/* 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>
<h2 className="text-sm md:text-xl lg:text-2xl font-bold text-white leading-snug mb-2 w-full md:w-9/12"> <span></span>
{articles[0].title} </div>
</h2> </div>
<p className="text-white text-xs flex items-center gap-2">
{articles[0].createdByName} -{" "} {/* play icon */}
<svg <div className="absolute top-3 right-3 w-8 h-8 bg-white/80 rounded-full flex items-center justify-center">
xmlns="http://www.w3.org/2000/svg" <svg
width="16" className="w-4 h-4 text-black"
height="16" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="currentColor"
> >
<g fill="none"> <path d="M8 5v14l11-7z" />
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /> </svg>
<path </div>
fill="currentColor" </Link>
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1" ))}
/> </div>
</g> </div>
</svg>{" "}
{new Date(articles[0].createdAt).toLocaleDateString( {/* 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", "id-ID",
{ {
day: "numeric", day: "2-digit",
month: "long", month: "long",
year: "numeric", year: "numeric",
} },
)} )}
</p> </span>
</div> </div>
</Link> </div>
</div> </Link>
)} </div>
) : (
<p className="text-gray-500">Loading...</p>
)}
<div className="md:col-span-1 lg:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-2"> {/* RIGHT SIDE RECENT POSTS */}
{articles.slice(1, 5).map((article, index) => ( <div>
<div key={index} className="relative"> <h3 className="text-lg font-semibold mb-3">Recent Posts</h3>
<Link href={`/detail/${article?.id}`}>
<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 <Image
src={ src={
article.thumbnailUrl || item.thumbnailUrl ||
article?.files?.[0]?.file_url || item.files?.[0]?.fileUrl ||
"/default-image.jpg" "/placeholder.jpg"
} }
alt={article.title} alt={item.title}
width={400} fill
height={240} className="object-cover"
className="w-full h-56 object-cover"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end"> </div>
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
{article?.categories?.[0]?.title || "TANPA KATEGORI"} <div className="flex flex-col">
</span> <p className="text-sm font-semibold line-clamp-2">
<h3 className="text-sm font-semibold text-white leading-snug mb-1"> {item.title}
{article.title} </p>
</h3> <span className="text-xs text-gray-500 mt-1">
</div> {new Date(item.publishedAt).toLocaleDateString("id-ID", {
</Link> day: "2-digit",
</div> month: "long",
year: "numeric",
})}
</span>
</div>
</Link>
))} ))}
</div> </div>
</div> </div>
</div>
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8"> {/* LOAD MORE */}
<Image <div className="flex justify-center my-6">
src="/image-kolom.png" <button className="text-gray-600 text-sm flex items-center gap-2 border-b pb-1">
alt="Berita Utama" <span>LOAD MORE</span>
fill <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
className="object-contain" <path
/> d="M12 5v14M5 12h14"
</div> 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 rounded-md">
<Image
src="/image-kolom.png"
alt="Kolom PPS Bottom Banner"
fill
className="object-contain"
/>
</div> </div>
</section> </section>
); );

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.categoryName || "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

@ -12,13 +12,15 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -76,8 +78,8 @@ export default function HeaderHealth() {
} }
const healthArticles = articles.filter((article) => const healthArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "kesehatan" category.title?.toLowerCase().includes("berita warga")
) )
); );
@ -108,9 +110,9 @@ export default function HeaderHealth() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
{mainArticle && ( {mainArticle && (
<div className="md:col-span-2 relative"> <div className="md:col-span-2 relative">
<Link href={`/detail/${mainArticle.id}`}> <Link href={`/details/${mainArticle.slug}`}>
<Image <Image
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"} src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
alt={mainArticle.title} alt={mainArticle.title}
width={800} width={800}
height={500} height={500}
@ -124,7 +126,9 @@ export default function HeaderHealth() {
{mainArticle.title} {mainArticle.title}
</h2> </h2>
<p className="text-white text-xs"> <p className="text-white text-xs">
{mainArticle.createdByName} -{" "} {mainArticle?.customCreatorName ||
mainArticle.createdByName}{" "}
-{" "}
{new Date(mainArticle.createdAt).toLocaleDateString( {new Date(mainArticle.createdAt).toLocaleDateString(
"id-ID", "id-ID",
{ {
@ -142,11 +146,11 @@ export default function HeaderHealth() {
<div className="grid grid-rows-2 gap-2"> <div className="grid grid-rows-2 gap-2">
{otherArticles.map((article, index) => ( {otherArticles.map((article, index) => (
<div key={index} className="relative"> <div key={index} className="relative">
<Link href={`/detail/${article.id}`}> <Link href={`/details/${article.slug}`}>
<Image <Image
src={ src={
article.thumbnailUrl || article.thumbnailUrl ||
article.files?.[0]?.file_url || article.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={article.title} alt={article.title}

View File

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import news from "../news"; import news from "../news";
import Link from "next/link"; import Link from "next/link";
import { getAdvertise } from "@/service/advertisement";
type Article = { type Article = {
id: number; id: number;
@ -12,13 +13,15 @@ type Article = {
description: string; description: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -32,6 +35,15 @@ const slugToLabel = (slug: string) => {
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1); return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
}; };
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
export default function HealthNews() { export default function HealthNews() {
const [activeTab, setActiveTab] = useState("comments"); const [activeTab, setActiveTab] = useState("comments");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -48,6 +60,36 @@ export default function HealthNews() {
const pathname = usePathname(); const pathname = usePathname();
const pathSegments = pathname.split("/").filter(Boolean); const pathSegments = pathname.split("/").filter(Boolean);
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
useEffect(() => {
initStateAdver();
}, []);
async function initStateAdver() {
const req = {
limit: 100,
page: 1,
sort: "desc",
sortBy: "created_at",
isPublish: true,
};
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);
}
}
const categorySlug = pathSegments[1]; const categorySlug = pathSegments[1];
const categoryLabel = slugToLabel(categorySlug); const categoryLabel = slugToLabel(categorySlug);
@ -81,11 +123,10 @@ export default function HealthNews() {
} }
const kesehatanArticles = articles.filter((article) => const kesehatanArticles = articles.filter((article) =>
article.categories?.some( article.categories?.some((category) =>
(category) => category.title.toLowerCase() === "kesehatan" category.title?.toLowerCase().includes("berita warga")
) )
); );
const itemsPerPage = 2; const itemsPerPage = 2;
const calculatedTotalPage = Math.ceil( const calculatedTotalPage = Math.ceil(
kesehatanArticles.length / itemsPerPage kesehatanArticles.length / itemsPerPage
@ -109,7 +150,7 @@ export default function HealthNews() {
<div key={item.id}> <div key={item.id}>
<Link <Link
className="flex flex-col md:flex-row gap-6" className="flex flex-col md:flex-row gap-6"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
<div className="relative w-full md:w-1/2 h-64"> <div className="relative w-full md:w-1/2 h-64">
<Image <Image
@ -131,7 +172,7 @@ export default function HealthNews() {
<div className="text-sm text-gray-600 mt-2"> <div className="text-sm text-gray-600 mt-2">
BY{" "} BY{" "}
<span className="text-green-600 font-semibold"> <span className="text-green-600 font-semibold">
{item.createdByName || "Admin"} {item?.customCreatorName || item.createdByName || "Admin"}
</span>{" "} </span>{" "}
{new Date(item.createdAt).toLocaleDateString("id-ID")} {new Date(item.createdAt).toLocaleDateString("id-ID")}
</div> </div>
@ -197,12 +238,32 @@ export default function HealthNews() {
<div className="space-y-6"> <div className="space-y-6">
<div className="w-full h-[400px] relative"> <div className="w-full h-[400px] relative">
<Image {bannerAd ? (
src="/advertisiment.png" <a
alt="Advertisement" href={bannerAd.redirectLink}
fill target="_blank"
className="object-cover rounded" rel="noopener noreferrer"
/> className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div> </div>
<div> <div>
@ -249,7 +310,7 @@ export default function HealthNews() {
<div key={item.id} className="flex gap-3 items-center"> <div key={item.id} className="flex gap-3 items-center">
<Link <Link
className="flex gap-3 items-center" className="flex gap-3 items-center"
href={`/detail/${item?.id}`} href={`/details/${item?.slug}`}
> >
<Image <Image
src={item.thumbnailUrl || "/no-image.jpg"} src={item.thumbnailUrl || "/no-image.jpg"}
@ -274,11 +335,11 @@ export default function HealthNews() {
</h2> </h2>
<div className=" w-full"> <div className=" w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={ src={
articles[0]?.thumbnailUrl || articles[0]?.thumbnailUrl ||
articles[0]?.files?.[0]?.file_url || articles[0]?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
@ -324,13 +385,13 @@ export default function HealthNews() {
<div key={index}> <div key={index}>
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
src={ src={
article?.thumbnailUrl || article?.thumbnailUrl ||
article?.files?.[0]?.file_url || article?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"article?.title"} alt={"article?.title"}

View File

@ -171,14 +171,17 @@ type Article = {
title: string; title: string;
description: string; description: string;
categoryName: string; categoryName: string;
publishedAt: string;
createdAt: string; createdAt: string;
createdByName: string; createdByName: string;
slug: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -237,7 +240,7 @@ export default function LatestandPopular() {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const currentArticles = articles.slice( const currentArticles = articles.slice(
startIndex, startIndex,
startIndex + ITEMS_PER_PAGE startIndex + ITEMS_PER_PAGE,
); );
return ( return (
<section className="bg-white py-10 px-4 md:px-10 w-full"> <section className="bg-white py-10 px-4 md:px-10 w-full">
@ -250,12 +253,12 @@ export default function LatestandPopular() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{currentArticles.map((article, index) => ( {currentArticles.map((article, index) => (
<div key={index}> <div key={index}>
<Link href={`/detail/${article?.id}`}> <Link href={`/details/${article?.slug}`}>
<div className="relative w-full aspect-video mb-3"> <div className="relative w-full aspect-video mb-3">
<Image <Image
src={ src={
article?.thumbnailUrl || article?.thumbnailUrl ||
article?.files?.[0]?.file_url || article?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={article?.title || "No title"} alt={article?.title || "No title"}
@ -280,7 +283,7 @@ export default function LatestandPopular() {
<div className="text-xs text-[#999999] mb-2 flex items-center gap-2"> <div className="text-xs text-[#999999] mb-2 flex items-center gap-2">
by{" "} by{" "}
<span className="text-[#31942E]"> <span className="text-[#31942E]">
{article.createdByName} {article?.customCreatorName || article.createdByName}
</span>{" "} </span>{" "}
|{" "} |{" "}
<div className="text-xs mt-1.5 text-[#A0A0A0] space-x-2 flex items-center"> <div className="text-xs mt-1.5 text-[#A0A0A0] space-x-2 flex items-center">
@ -299,14 +302,18 @@ export default function LatestandPopular() {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(article?.createdAt).toLocaleDateString( {new Date(article?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4" className="h-4 w-4"
@ -367,11 +374,11 @@ export default function LatestandPopular() {
</h2> </h2>
<div className=" w-full"> <div className=" w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={ src={
articles[0]?.thumbnailUrl || articles[0]?.thumbnailUrl ||
articles[0]?.files?.[0]?.file_url || articles[0]?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
@ -399,14 +406,18 @@ export default function LatestandPopular() {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>
@ -417,13 +428,13 @@ export default function LatestandPopular() {
<div key={index} className="flex gap-3"> <div key={index} className="flex gap-3">
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
src={ src={
article?.thumbnailUrl || article?.thumbnailUrl ||
article?.files?.[0]?.file_url || article?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={"article?.title"} alt={"article?.title"}
@ -450,14 +461,18 @@ export default function LatestandPopular() {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>

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 News() {
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 POPULER</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.categoryName || "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

@ -176,15 +176,17 @@ type Article = {
id: number; id: number;
title: string; title: string;
description: string; description: string;
publishedAt: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
@ -249,9 +251,9 @@ export default function Latest({ id }: { id: number }) {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 "> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 ">
<div className=" w-full"> <div className=" w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={articles[0]?.files?.[0]?.file_url || "/nodata.png"} src={articles[0]?.files?.[0]?.fileUrl || "/nodata.png"}
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
fill fill
sizes="(max-width: 1024px) 100vw, 33vw" sizes="(max-width: 1024px) 100vw, 33vw"
@ -277,14 +279,18 @@ export default function Latest({ id }: { id: number }) {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>
@ -295,7 +301,7 @@ export default function Latest({ id }: { id: number }) {
<div key={index} className="flex gap-3"> <div key={index} className="flex gap-3">
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
@ -324,14 +330,18 @@ export default function Latest({ id }: { id: number }) {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>
@ -342,10 +352,10 @@ export default function Latest({ id }: { id: number }) {
<div className="w-full"> <div className="w-full">
<div className="relative w-full aspect-video mb-5"> <div className="relative w-full aspect-video mb-5">
<Link href={`/detail/${articles[0]?.id}`}> <Link href={`/details/${articles[0]?.slug}`}>
<Image <Image
src={ src={
articles[0]?.files?.[0]?.file_url || "/default-image.jpg" articles[0]?.files?.[0]?.fileUrl || "/default-image.jpg"
} }
alt={"articles[0]?.title"} alt={"articles[0]?.title"}
fill fill
@ -372,14 +382,18 @@ export default function Latest({ id }: { id: number }) {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>
@ -390,7 +404,7 @@ export default function Latest({ id }: { id: number }) {
<div key={index} className="flex gap-3"> <div key={index} className="flex gap-3">
<Link <Link
className="flex gap-3" className="flex gap-3"
href={`/detail/${article?.id}`} href={`/details/${article?.slug}`}
> >
<div className="relative w-[120px] h-[86px] shrink-0"> <div className="relative w-[120px] h-[86px] shrink-0">
<Image <Image
@ -419,14 +433,18 @@ export default function Latest({ id }: { id: number }) {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</p> </p>
</div> </div>
</Link> </Link>
@ -474,10 +492,10 @@ export default function Latest({ id }: { id: number }) {
<div className="space-y-8"> <div className="space-y-8">
{popularPosts.slice(1, 5).map((post, index) => ( {popularPosts.slice(1, 5).map((post, index) => (
<div key={index} className="space-y-3"> <div key={index} className="space-y-3">
<Link href={`/detail/${post?.id}`}> <Link href={`/details/${post?.slug}`}>
<div <div
className={`flex gap-4 ${ className={`flex gap-4 ${
post.files?.[0]?.file_url post.files?.[0]?.fileUrl
? "flex-col md:flex-row" ? "flex-col md:flex-row"
: "flex-col" : "flex-col"
}`} }`}
@ -501,14 +519,18 @@ export default function Latest({ id }: { id: number }) {
/> />
</g> </g>
</svg>{" "} </svg>{" "}
{new Date(articles[0]?.createdAt).toLocaleDateString( {new Date(articles[0]?.publishedAt)
"id-ID", .toLocaleString("id-ID", {
{
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} hour: "2-digit",
)} minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4" className="h-4 w-4"

View File

@ -1,229 +1,170 @@
"use client"; "use client";
import { useState } from "react";
import { Search } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Menu, Lock, Search } from "lucide-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
const Navbar = () => { export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => setIsOpen(!isOpen);
const pathname = usePathname(); const pathname = usePathname();
const isActive = (href: any) => {
const navLinks = [ return pathname === href || pathname.startsWith(href + "/");
{ href: "/", label: "BERANDA" },
{ href: "/category/development", label: "PEMBANGUNAN" },
{ href: "/category/health", label: "KESEHATAN" },
{ href: "/category/citizen-news", label: "BERITA WARGA" },
];
const isActive = (href: string) => {
if (href === "/") {
return pathname === "/";
}
return pathname.startsWith(href);
}; };
return ( return (
<nav className="w-full bg-white shadow-md"> <div className="w-full bg-white py-4 border-b">
<div className="max-w-7xl mx-auto py-2 px-0 md:px-4 flex flex-col md:flex-row justify-end items-start md:justify-between md:items-center gap-4 bg-[#308A2E]"> <div className="max-w-screen-xl mx-auto flex flex-col justify-between px-4">
<div className="flex items-center gap-4 w-full md:w-auto mx-3 md:mx-5"> {/* Left: Logo */}
<div className="flex gap-3 text-xs mx-3 text-white"> <div className="flex flex-row justify-between mb-3">
<Link href="/about" className="hover:text-yellow-400 "> <div className="flex items-center">
About <Image
</Link> src="/mikul-news-logo.png"
<Link href="/advertise" className="hover:text-yellow-400"> alt="Kritik Tajam Logo"
Advertise width={140}
</Link> height={100}
<Link href="/privacy" className="hover:text-yellow-400"> />
Privacy & Policy
</Link>
<Link href="/contact" className="hover:text-yellow-400">
Contact
</Link>
</div> </div>
</div> <div className="flex items-center gap-6">
{/* Social Icons */}
<div className="flex items-center gap-5 text-sm whitespace-nowrap text-white mx-5"> <div className="hidden md:flex items-center gap-5 text-black text-xl">
<div className="hidden md:block min-w-fit whitespace-nowrap text-white text-xs"> <Link href="#">
{new Date().toLocaleDateString("id-ID", { <svg
weekday: "long", xmlns="http://www.w3.org/2000/svg"
year: "numeric", width="18"
month: "long", viewBox="0 0 24 24"
day: "numeric", >
})}
</div>
<div className="flex gap-3 text-white text-lg">
<Link href="#" aria-label="Facebook">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
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>
<Link href="#" aria-label="Twitter">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
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>
<Link href="#" aria-label="Google" className="text-[#F5F5F5]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M7.796 14.333v-2.618h7.211c.066.382.12.763.12 1.265c0 4.364-2.923 7.462-7.33 7.462A7.63 7.63 0 0 1 .16 12.806a7.63 7.63 0 0 1 7.636-7.637c2.062 0 3.786.753 5.117 1.997L10.84 9.162c-.567-.546-1.56-1.178-3.044-1.178c-2.607 0-4.734 2.16-4.734 4.822s2.127 4.821 4.734 4.821c3.022 0 4.157-2.17 4.331-3.294zm13.27-2.6H23.2v2.134h-2.133V16h-2.134v-2.133H16.8v-2.134h2.133V9.6h2.134z"
// clip-rule="evenodd"
/>
</svg>
</Link>
<Link href="#" aria-label="Pinterest">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<defs>
<path <path
id="akarIconsPinterestFill0" fill="currentColor"
fill="#fff" 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"
d="M0 0h24v24H0z"
/> />
</defs> </svg>
<g fill="none">
<g
// clip-path="url(#akarIconsPinterestFill1)"
>
<g
// clip-path="url(#akarIconsPinterestFill2)"
>
<path
fill="currentColor"
d="M0 12c0 5.123 3.211 9.497 7.73 11.218c-.11-.937-.227-2.482.025-3.566c.217-.932 1.401-5.938 1.401-5.938s-.357-.715-.357-1.774c0-1.66.962-2.9 2.161-2.9c1.02 0 1.512.765 1.512 1.682c0 1.025-.653 2.557-.99 3.978c-.281 1.189.597 2.159 1.769 2.159c2.123 0 3.756-2.239 3.756-5.471c0-2.861-2.056-4.86-4.991-4.86c-3.398 0-5.393 2.549-5.393 5.184c0 1.027.395 2.127.889 2.726a.36.36 0 0 1 .083.343c-.091.378-.293 1.189-.332 1.355c-.053.218-.173.265-.4.159c-1.492-.694-2.424-2.875-2.424-4.627c0-3.769 2.737-7.229 7.892-7.229c4.144 0 7.365 2.953 7.365 6.899c0 4.117-2.595 7.431-6.199 7.431c-1.211 0-2.348-.63-2.738-1.373c0 0-.599 2.282-.744 2.84c-.282 1.084-1.064 2.456-1.549 3.235C9.584 23.815 10.77 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0S0 5.373 0 12"
/>
</g>
</g>
<defs>
<clipPath id="akarIconsPinterestFill1">
<use href="#akarIconsPinterestFill0" />
</clipPath>
<clipPath id="akarIconsPinterestFill2">
<use href="#akarIconsPinterestFill0" />
</clipPath>
</defs>
</g>
</svg>
</Link>
<Link href="#" aria-label="Vk">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M23.45 5.948c.166-.546 0-.948-.795-.948H20.03c-.668 0-.976.347-1.143.73c0 0-1.335 3.196-3.226 5.272c-.612.602-.89.793-1.224.793c-.167 0-.418-.191-.418-.738V5.948c0-.656-.184-.948-.74-.948H9.151c-.417 0-.668.304-.668.593c0 .621.946.765 1.043 2.513v3.798c0 .833-.153.984-.487.984c-.89 0-3.055-3.211-4.34-6.885C4.45 5.288 4.198 5 3.527 5H.9c-.75 0-.9.347-.9.73c0 .682.89 4.07 4.145 8.551C6.315 17.341 9.37 19 12.153 19c1.669 0 1.875-.368 1.875-1.003v-2.313c0-.737.158-.884.687-.884c.39 0 1.057.192 2.615 1.667C19.11 18.216 19.403 19 20.405 19h2.625c.75 0 1.126-.368.91-1.096c-.238-.724-1.088-1.775-2.215-3.022c-.612-.71-1.53-1.475-1.809-1.858c-.389-.491-.278-.71 0-1.147c0 0 3.2-4.426 3.533-5.929"
// clip-rule="evenodd"
/>
</svg>
</Link>
<Link href="#" aria-label="Wifi">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 21q-1.05 0-1.775-.725T9.5 18.5t.725-1.775T12 16t1.775.725t.725 1.775t-.725 1.775T12 21m-5.65-5.65l-2.1-2.15q1.475-1.475 3.463-2.337T12 10t4.288.875t3.462 2.375l-2.1 2.1q-1.1-1.1-2.55-1.725T12 13t-3.1.625t-2.55 1.725M2.1 11.1L0 9q2.3-2.35 5.375-3.675T12 4t6.625 1.325T24 9l-2.1 2.1q-1.925-1.925-4.462-3.012T12 7T6.563 8.088T2.1 11.1"
/>
</svg>
</Link>
</div>
<Link
href="/auth"
className="hover:underline flex items-center gap-1"
>
<Lock className="w-3 h-3" />
Login
</Link>
</div>
</div>
<div className="bg-[#31942E] text-white">
<div className="flex items-start justify-start md:justify-center md:items-center px-4 py-3 md:px-8 md:py-4">
{/* Toggle Menu (Mobile Only) */}
<button
className="md:hidden flex items-center"
onClick={toggleMenu}
aria-label="Toggle menu"
>
<Menu className="h-6 w-6 text-white" />
</button>
{/* Navigation Links (Desktop Only) */}
<div className="hidden md:flex items-center md:gap-x-32 font-semibold text-sm">
{navLinks.map((link, idx) => (
<Link
key={idx}
href={link.href}
className={`pl-4 pr-4 ${
isActive(link.href)
? "text-yellow-400"
: "hover:text-yellow-400 text-white"
}`}
>
{link.label}
</Link> </Link>
))}
<Search className="w-5 h-5 hover:text-yellow-400 cursor-pointer ml-4" /> <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>
<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>
</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={
isActive("/")
? "text-green-500 underline"
: "text-black hover:text-green-500"
}
>
Beranda
</Link>
{isOpen && ( <Link
<div className="md:hidden px-4 pb-4 flex flex-col gap-2 font-semibold text-sm bg-[#31942E]"> href="/category/citizen-news"
{navLinks.map((link, idx) => ( className={
<Link isActive("/category/citizen-news")
key={idx} ? "text-green-500 underline"
href={link.href} : "text-black hover:text-green-500"
className={`${ }
isActive(link.href) >
? "text-yellow-400" Berita Warga
: "hover:text-yellow-400 text-white" </Link>
}`}
<Link
href="/category/development"
className={
isActive("/category/development")
? "text-green-500 underline"
: "text-black hover:text-green-500"
}
>
Pembangunan
</Link>
<Link
href="/category/health"
className={
isActive("/category/health")
? "text-green-500 underline"
: "text-black hover:text-green-500"
}
>
Kesehatan
</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"
> >
{link.label} <path
</Link> 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-green-600 text-white px-5 py-2 rounded-full text-sm font-semibold">
LOGIN
</button>
</Link>
</div> </div>
)} </div>
</div> </div>
</nav> </div>
); );
}; }
export default Navbar;

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import { getAdvertise } from "@/service/advertisement";
import { getListArticle } from "@/service/article"; import { getListArticle } from "@/service/article";
import { Clock } from "lucide-react"; import { Clock } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
@ -9,19 +10,31 @@ type postsData = {
id: number; id: number;
title: string; title: string;
description: string; description: string;
publishedAt: string;
categoryName: string; categoryName: string;
createdAt: string; createdAt: string;
slug: string;
createdByName: string; createdByName: string;
customCreatorName: string;
thumbnailUrl: string; thumbnailUrl: string;
categories: { categories: {
title: string; title: string;
}[]; }[];
files: { files: {
file_url: string; fileUrl: string;
file_alt: string; file_alt: string;
}[]; }[];
}; };
type Advertise = {
id: number;
title: string;
description: string;
placement: string;
contentFileUrl: string;
redirectLink: string;
};
export default function Beranda() { export default function Beranda() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1); const [totalPage, setTotalPage] = useState(1);
@ -34,6 +47,36 @@ export default function Beranda() {
endDate: null, endDate: null,
}); });
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
useEffect(() => {
initStateAdver();
}, []);
async function initStateAdver() {
const req = {
limit: 100,
page: 1,
sort: "desc",
sortBy: "created_at",
isPublish: true,
};
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 === "banner");
if (banner) {
setBannerAd(banner);
}
} catch (err) {
console.error("Error fetching advertisement:", err);
}
}
useEffect(() => { useEffect(() => {
initState(); initState();
}, [page, showData, startDateValue, selectedCategories]); }, [page, showData, startDateValue, selectedCategories]);
@ -76,18 +119,18 @@ export default function Beranda() {
{posts {posts
.filter((post) => .filter((post) =>
post.categories?.some( post.categories?.some(
(category) => category.title.toLowerCase() === "pembangunan" (category) => category.title.toLowerCase() === "pembangunan",
) ),
) )
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter .slice(0, 6) // Ambil 4 artikel pertama setelah difilter
.map((post, index) => ( .map((post, index) => (
<div key={index} className="bg-white overflow-hidden"> <div key={index} className="bg-white overflow-hidden">
<Link href={`/detail/${post?.id}`}> <Link href={`/details/${post?.slug}`}>
<div className="relative"> <div className="relative">
<Image <Image
src={ src={
post.thumbnailUrl || post.thumbnailUrl ||
post?.files?.[0]?.file_url || post?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={post.title} alt={post.title}
@ -107,12 +150,19 @@ export default function Beranda() {
<div className="text-xs flex items-center gap-2"> <div className="text-xs flex items-center gap-2">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
<span> <span>
{post.createdByName} -{" "} {post?.customCreatorName || post.createdByName} -{" "}
{new Date(post.createdAt).toLocaleDateString("id-ID", { {new Date(post.publishedAt)
day: "numeric", .toLocaleString("id-ID", {
month: "long", day: "numeric",
year: "numeric", month: "long",
})} year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Jakarta",
})
.replace("pukul ", "")}{" "}
WIB
</span> </span>
</div> </div>
</div> </div>
@ -121,13 +171,33 @@ export default function Beranda() {
</div> </div>
))} ))}
</div> </div>
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border"> <div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
<Image {bannerAd ? (
src="/image-kolom.png" <a
alt="Berita Utama" href={bannerAd.redirectLink}
fill target="_blank"
className="object-cover" rel="noopener noreferrer"
/> className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/image-kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div> </div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-1 gap-2 bg-white border-b-2 pt-2 "> <div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-1 gap-2 bg-white border-b-2 pt-2 ">
<h2 className="text-sm font-bold">Kesehatan</h2> <h2 className="text-sm font-bold">Kesehatan</h2>
@ -145,18 +215,18 @@ export default function Beranda() {
{posts {posts
.filter((post) => .filter((post) =>
post.categories?.some( post.categories?.some(
(category) => category.title.toLowerCase() === "kesehatan" (category) => category.title.toLowerCase() === "kesehatan",
) ),
) )
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter .slice(0, 6) // Ambil 4 artikel pertama setelah difilter
.map((post, index) => ( .map((post, index) => (
<div key={index} className="bg-white overflow-hidden"> <div key={index} className="bg-white overflow-hidden">
<Link href={`/detail/${post?.id}`}> <Link href={`/details/${post?.slug}`}>
<div className="relative"> <div className="relative">
<Image <Image
src={ src={
post.thumbnailUrl || post.thumbnailUrl ||
post?.files?.[0]?.file_url || post?.files?.[0]?.fileUrl ||
"/default-image.jpg" "/default-image.jpg"
} }
alt={post.title} alt={post.title}
@ -176,7 +246,7 @@ export default function Beranda() {
<div className="text-xs flex items-center gap-2"> <div className="text-xs flex items-center gap-2">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
<span> <span>
{post.createdByName} -{" "} {post?.customCreatorName || post.createdByName} -{" "}
{new Date(post.createdAt).toLocaleDateString("id-ID", { {new Date(post.createdAt).toLocaleDateString("id-ID", {
day: "numeric", day: "numeric",
month: "long", month: "long",
@ -190,13 +260,33 @@ export default function Beranda() {
</div> </div>
))} ))}
</div> </div>
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border"> <div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
<Image {bannerAd ? (
src="/image-kolom.png" <a
alt="Berita Utama" href={bannerAd.redirectLink}
fill target="_blank"
className="object-cover" rel="noopener noreferrer"
/> className="block w-full"
>
<div className="relative w-full h-[350px] flex justify-center">
<Image
src={bannerAd.contentFileUrl}
alt={bannerAd.title || "Iklan Banner"}
width={1200} // ukuran dasar untuk responsive
height={350}
className="object-cover w-full h-full"
/>
</div>
</a>
) : (
<Image
src="/image-kolom.png"
alt="Berita Utama"
width={1200}
height={188}
className="object-contain w-full h-[188px]"
/>
)}
</div> </div>
</section> </section>
); );

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.categoryName || "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-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

@ -106,7 +106,7 @@ export default function DashboardContainer() {
async function initState() { async function initState() {
const req = { const req = {
limit: "4", limit: "5",
page: page, page: page,
search: "", search: "",
}; };
@ -207,11 +207,15 @@ export default function DashboardContainer() {
<p className="text-slate-600">{username}</p> <p className="text-slate-600">{username}</p>
<div className="flex space-x-6 pt-2"> <div className="flex space-x-6 pt-2">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-blue-600">{summary?.totalToday}</p> <p className="text-2xl font-bold text-blue-600">
{summary?.totalToday}
</p>
<p className="text-sm text-slate-500">Today</p> <p className="text-sm text-slate-500">Today</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-purple-600">{summary?.totalThisWeek}</p> <p className="text-2xl font-bold text-purple-600">
{summary?.totalThisWeek}
</p>
<p className="text-sm text-slate-500">This Week</p> <p className="text-sm text-slate-500">This Week</p>
</div> </div>
</div> </div>
@ -234,7 +238,9 @@ export default function DashboardContainer() {
<DashboardSpeecIcon /> <DashboardSpeecIcon />
</div> </div>
<div> <div>
<p className="text-3xl font-bold text-slate-800">{summary?.totalAll}</p> <p className="text-3xl font-bold text-slate-800">
{summary?.totalAll}
</p>
<p className="text-sm text-slate-500">Total Posts</p> <p className="text-sm text-slate-500">Total Posts</p>
</div> </div>
</div> </div>
@ -252,7 +258,9 @@ export default function DashboardContainer() {
<DashboardConnectIcon /> <DashboardConnectIcon />
</div> </div>
<div> <div>
<p className="text-3xl font-bold text-slate-800">{summary?.totalViews}</p> <p className="text-3xl font-bold text-slate-800">
{summary?.totalViews}
</p>
<p className="text-sm text-slate-500">Total Views</p> <p className="text-sm text-slate-500">Total Views</p>
</div> </div>
</div> </div>
@ -270,7 +278,9 @@ export default function DashboardContainer() {
<DashboardShareIcon /> <DashboardShareIcon />
</div> </div>
<div> <div>
<p className="text-3xl font-bold text-slate-800">{summary?.totalShares}</p> <p className="text-3xl font-bold text-slate-800">
{summary?.totalShares}
</p>
<p className="text-sm text-slate-500">Total Shares</p> <p className="text-sm text-slate-500">Total Shares</p>
</div> </div>
</div> </div>
@ -288,7 +298,9 @@ export default function DashboardContainer() {
<DashboardCommentIcon size={40} /> <DashboardCommentIcon size={40} />
</div> </div>
<div> <div>
<p className="text-3xl font-bold text-slate-800">{summary?.totalComments}</p> <p className="text-3xl font-bold text-slate-800">
{summary?.totalComments}
</p>
<p className="text-sm text-slate-500">Total Comments</p> <p className="text-sm text-slate-500">Total Comments</p>
</div> </div>
</div> </div>
@ -305,13 +317,20 @@ export default function DashboardContainer() {
transition={{ delay: 0.6 }} transition={{ delay: 0.6 }}
> >
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">Analytics Overview</h3> <h3 className="text-lg font-semibold text-slate-800">
Analytics Overview
</h3>
<div className="flex space-x-4"> <div className="flex space-x-4">
{options.map((option) => ( {options.map((option) => (
<label key={option.value} className="flex items-center space-x-2"> <label
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox <Checkbox
checked={analyticsView.includes(option.value)} checked={analyticsView.includes(option.value)}
onCheckedChange={(checked) => handleChange(option.value, checked as boolean)} onCheckedChange={(checked) =>
handleChange(option.value, checked as boolean)
}
/> />
<span className="text-sm text-slate-600">{option.label}</span> <span className="text-sm text-slate-600">{option.label}</span>
</label> </label>
@ -319,12 +338,12 @@ export default function DashboardContainer() {
</div> </div>
</div> </div>
<div className="h-80"> <div className="h-80">
<ApexChartColumn <ApexChartColumn
type="monthly" type="monthly"
date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`} date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`}
view={analyticsView} view={analyticsView}
/> />
</div> </div>
</motion.div> </motion.div>
{/* Recent Articles */} {/* Recent Articles */}
@ -335,12 +354,14 @@ export default function DashboardContainer() {
transition={{ delay: 0.7 }} transition={{ delay: 0.7 }}
> >
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">Recent Articles</h3> <h3 className="text-lg font-semibold text-slate-800">
<Link href="/admin/article/create"> Recent Articles
</h3>
{/* <Link href="/admin/article/create">
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg"> <Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
Create Article Create Article
</Button> </Button>
</Link> </Link> */}
</div> </div>
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin"> <div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">

View File

@ -10,7 +10,7 @@ import {
} from "@/components/icons"; } from "@/components/icons";
import { close, error, loading, success, successToast } from "@/config/swal"; import { close, error, loading, success, successToast } from "@/config/swal";
import { Article } from "@/types/globals"; import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global"; import { convertDateFormat, formatDate } from "@/utils/global";
import Link from "next/link"; import Link from "next/link";
import { Key, useCallback, useEffect, useState } from "react"; import { Key, useCallback, useEffect, useState } from "react";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
@ -46,6 +46,7 @@ import {
TableCell, TableCell,
} from "@/components/ui/table"; } from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination"; import CustomPagination from "../layout/custom-pagination";
import DatePicker from "react-datepicker";
const columns = [ const columns = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
@ -53,17 +54,18 @@ const columns = [
{ name: "Banner", uid: "isBanner" }, { name: "Banner", uid: "isBanner" },
{ name: "Kategori", uid: "category" }, { name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" }, { name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" }, { name: "Kreator", uid: "customCreatorName" },
{ name: "Status", uid: "isPublish" }, { name: "Status", uid: "isPublish" },
{ name: "Aksi", uid: "actions" }, { name: "Aksi", uid: "actions" },
]; ];
const columnsOtherRole = [ const columnsOtherRole = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
{ name: "Judul", uid: "title" }, { name: "Judul", uid: "title" },
{ name: "Source", uid: "source" },
{ name: "Kategori", uid: "category" }, { name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" }, { name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" }, { name: "Kreator", uid: "customCreatorName" },
{ name: "Status", uid: "isPublish" }, { name: "Status", uid: "publishStatus" },
{ name: "Aksi", uid: "actions" }, { name: "Aksi", uid: "actions" },
]; ];
@ -84,7 +86,10 @@ export default function ArticleTable() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [categories, setCategories] = useState<any>([]); const [categories, setCategories] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>(""); const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({ const [selectedCategoryId, setSelectedCategoryId] = useState<any>("");
const [selectedSource, setSelectedSource] = useState<any>("");
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [dateRange, setDateRange] = useState<any>({
startDate: null, startDate: null,
endDate: null, endDate: null,
}); });
@ -98,24 +103,45 @@ export default function ArticleTable() {
const res = await getArticleByCategory(); const res = await getArticleByCategory();
const data = res?.data?.data; const data = res?.data?.data;
setCategories(data); setCategories(data);
console.log("category", data);
} }
useEffect(() => {
initState();
}, [
page,
showData,
search,
selectedCategoryId,
selectedSource,
dateRange,
selectedStatus,
]);
async function initState() { async function initState() {
loading(); loading();
const req = { const req = {
limit: showData, limit: showData,
page: page, page: page,
search: search, search: search,
categorySlug: Array.from(selectedCategories).join(","), category: selectedCategoryId || "",
source: selectedSource || "",
isPublish:
selectedStatus !== "" ? selectedStatus === "publish" : undefined,
startDate: formatDate(dateRange.startDate),
endDate: formatDate(dateRange.endDate),
sort: "desc", sort: "desc",
sortBy: "created_at", sortBy: "created_at",
}; };
const res = await getArticlePagination(req); const res = await getArticlePagination(req);
await getTableNumber(parseInt(showData), res.data?.data);
let data = res.data?.data || [];
await getTableNumber(parseInt(showData), data);
setTotalPage(res?.data?.meta?.totalPage); setTotalPage(res?.data?.meta?.totalPage);
close(); close();
} }
// panggil ulang setiap state berubah // panggil ulang setiap state berubah
useEffect(() => { useEffect(() => {
initState(); initState();
@ -173,11 +199,11 @@ export default function ArticleTable() {
initState(); initState();
}; };
const copyUrlArticle = async (id: number, slug: string) => { const copyUrlArticle = async (slug: any) => {
const url = const url =
`${window.location.protocol}//${window.location.host}` + `${window.location.protocol}//${window.location.host}` +
"/news/detail/" + "/details/" +
`${id}-${slug}`; `${slug}`;
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard"); successToast("Success", "Article Copy to Clipboard");
@ -192,7 +218,16 @@ export default function ArticleTable() {
const cellValue = article[columnKey as keyof any]; const cellValue = article[columnKey as keyof any];
switch (columnKey) { switch (columnKey) {
case "isPublish": case "customCreatorName":
return (
<p>
{article.customCreatorName &&
article.customCreatorName.trim() !== ""
? article.customCreatorName
: article.createdByName}
</p>
);
case "publishStatus":
return ( return (
// <Chip // <Chip
// className="capitalize " // className="capitalize "
@ -204,7 +239,7 @@ export default function ArticleTable() {
// {article.status} // {article.status}
// </div> // </div>
// </Chip> // </Chip>
<p>{article.isPublish ? "Publish" : "Draft"}</p> <p>{article.publishStatus}</p>
); );
case "isBanner": case "isBanner":
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>; return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
@ -229,7 +264,7 @@ export default function ArticleTable() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
<DropdownMenuItem <DropdownMenuItem
onClick={() => copyUrlArticle(article.id, article.slug)} onClick={() => copyUrlArticle(article.slug)}
> >
<CopyIcon className="mr-2 h-4 w-4" /> <CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article Copy Url Article
@ -245,18 +280,15 @@ export default function ArticleTable() {
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
{(username === "admin-mabes" || <DropdownMenuItem asChild>
Number(userId) === article.createdById) && ( <Link
<DropdownMenuItem asChild> href={`/admin/article/edit/${article.id}`}
<Link className="flex items-center"
href={`/admin/article/edit/${article.id}`} >
className="flex items-center" <CreateIconIon className="mr-2 h-4 w-4" />
> Edit
<CreateIconIon className="mr-2 h-4 w-4" /> </Link>
Edit </DropdownMenuItem>
</Link>
</DropdownMenuItem>
)}
{username === "admin-mabes" && ( {username === "admin-mabes" && (
<DropdownMenuItem <DropdownMenuItem
@ -271,13 +303,10 @@ export default function ArticleTable() {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{(username === "admin-mabes" || <DropdownMenuItem onClick={() => handleDelete(article.id)}>
Number(userId) === article.createdById) && ( <DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
<DropdownMenuItem onClick={() => handleDelete(article.id)}> Delete
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" /> </DropdownMenuItem>
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@ -287,7 +316,7 @@ export default function ArticleTable() {
return cellValue; return cellValue;
} }
}, },
[article, page] [article, page],
); );
let typingTimer: NodeJS.Timeout; let typingTimer: NodeJS.Timeout;
@ -346,36 +375,78 @@ export default function ArticleTable() {
<div className="flex flex-col gap-1 w-full lg:w-[230px]"> <div className="flex flex-col gap-1 w-full lg:w-[230px]">
<p className="font-semibold text-sm">Kategori</p> <p className="font-semibold text-sm">Kategori</p>
<Select <Select
value={selectedCategories} value={selectedCategoryId}
onValueChange={(value) => setSelectedCategories(value)} onValueChange={(value) => setSelectedCategoryId(value)} // simpan ID
> >
<SelectTrigger className="w-full text-sm border"> <SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Kategori" /> <SelectValue placeholder="Kategori" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{categories {categories
?.filter((category: any) => category.slug != null) ?.filter((category: any) => category.title != null)
.map((category: any) => ( .map((category: any) => (
<SelectItem key={category.slug} value={category.slug}> <SelectItem
key={category.id}
value={category.id.toString()} // kirim ID, bukan title
>
{category.title} {category.title}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]"> <div className="flex flex-col gap-1 w-full lg:w-[150px]">
<p className="font-semibold text-sm">Source</p>
<Select
value={selectedSource}
onValueChange={(value) => setSelectedSource(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Pilih Source" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal">INTERNAL</SelectItem>
<SelectItem value="external">EXTERNAL</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[150px]">
<p className="font-semibold text-sm">Status</p>
<Select
value={selectedStatus}
onValueChange={(value) => setSelectedStatus(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Pilih Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="publish">PUBLISH</SelectItem>
<SelectItem value="draft">DRAFT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[240px]">
<p className="font-semibold text-sm">Tanggal</p> <p className="font-semibold text-sm">Tanggal</p>
<Datepicker <DatePicker
value={startDateValue} selectsRange
displayFormat="DD/MM/YYYY" startDate={dateRange.startDate}
onChange={(e: any) => setStartDateValue(e)} endDate={dateRange.endDate}
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300" onChange={(update: [Date | null, Date | null]) => {
setDateRange({
startDate: update[0],
endDate: update[1],
});
}}
isClearable
dateFormat="dd/MM/yyyy"
className="z-50 w-full text-sm bg-transparent border border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
placeholderText="Pilih rentang tanggal"
/> />
</div> */} </div>
</div> </div>
<div className="w-full overflow-x-hidden"> <div className="w-full overflow-x-hidden">
<div className="w-full mx-auto overflow-x-hidden"> <div className="w-full overflow-x-auto">
<Table className="w-full table-fixed border text-sm"> <Table className="min-w-[1000px] w-full table-auto border text-sm">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{(username === "admin-mabes" {(username === "admin-mabes"
@ -384,7 +455,18 @@ export default function ArticleTable() {
).map((column) => ( ).map((column) => (
<TableHead <TableHead
key={column.uid} 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} {column.name}
</TableHead> </TableHead>
@ -401,7 +483,17 @@ export default function ArticleTable() {
).map((column) => ( ).map((column) => (
<TableCell <TableCell
key={column.uid} 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)} {renderCell(item, column.uid)}
</TableCell> </TableCell>

View File

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
@ -15,7 +15,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };

View File

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

3000
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tinymce/tinymce-react": "^6.2.1",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
"axios": "^1.10.0", "axios": "^1.10.0",
@ -38,12 +39,12 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lightningcss": "^1.30.1", "lightningcss": "^1.30.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"next": "15.3.4", "next": "^16.1.1",
"react": "^19.0.0", "react": "^19.2.3",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
"react-datepicker": "^8.4.0", "react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0", "react-day-picker": "^9.7.0",
"react-dom": "^19.0.0", "react-dom": "^19.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.59.0", "react-hook-form": "^7.59.0",
"react-password-checklist": "^1.8.1", "react-password-checklist": "^1.8.1",

BIN
public/mikul-news-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
public/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/yt-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,6 +1,11 @@
import { PaginationRequest } from "@/types/globals"; import { PaginationRequest } from "@/types/globals";
import { httpGet } from "./http-config/http-base-services"; import { httpGet } from "./http-config/http-base-services";
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services"; import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
export async function getListArticle(props: PaginationRequest) { export async function getListArticle(props: PaginationRequest) {
const { const {
@ -16,13 +21,14 @@ export async function getListArticle(props: PaginationRequest) {
categorySlug, categorySlug,
isBanner, isBanner,
} = props; } = props;
return await httpGet( return await httpGet(
`/articles?limit=${limit}&page=${page}&isPublish=${ `/articles?limit=${limit}&page=${page}&isPublish=${
isPublish === undefined ? "" : isPublish isPublish === undefined ? "" : isPublish
}&title=${search}&startDate=${startDate || ""}&endDate=${ }&title=${search}&startDate=${startDate || ""}&endDate=${
endDate || "" endDate || ""
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${ }&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
sort || "asc" sort || "desc"
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`, }&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
null null
); );
@ -40,13 +46,20 @@ export async function getArticlePagination(props: PaginationRequest) {
sort, sort,
categorySlug, categorySlug,
isBanner, isBanner,
isPublish,
source,
} = props; } = props;
return await httpGetInterceptor( return await httpGetInterceptor(
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${startDate || ""}&endDate=${ `/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
endDate || "" startDate || ""
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${ }&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
sort || "asc" source || ""
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}` }&isPublish=${isPublish !== undefined ? isPublish : ""}&sortBy=${
sortBy || "created_at"
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
isBanner || ""
}`
); );
} }
@ -81,6 +94,11 @@ export async function updateArticle(id: string, data: any) {
return await httpPutInterceptor(pathUrl, data); return await httpPutInterceptor(pathUrl, data);
} }
export async function unPublishArticle(id: string, data: any) {
const pathUrl = `/articles/${id}/unpublish`;
return await httpPutInterceptor(pathUrl, data);
}
export async function getArticleById(id: any) { export async function getArticleById(id: any) {
const headers = { const headers = {
"content-type": "application/json", "content-type": "application/json",
@ -88,6 +106,13 @@ export async function getArticleById(id: any) {
return await httpGet(`/articles/${id}`, headers); 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) { export async function deleteArticle(id: string) {
const headers = { const headers = {
"content-type": "application/json", "content-type": "application/json",
@ -111,6 +136,13 @@ export async function uploadArticleFile(id: string, data: any) {
return await httpPostInterceptor(`/article-files/${id}`, data, headers); return await httpPostInterceptor(`/article-files/${id}`, data, headers);
} }
export async function getArticleFiles() {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/article-files`, headers);
}
export async function uploadArticleThumbnail(id: string, data: any) { export async function uploadArticleThumbnail(id: string, data: any) {
const headers = { const headers = {
"content-type": "multipart/form-data", "content-type": "multipart/form-data",

View File

@ -1,12 +1,12 @@
import axios from "axios"; import axios from "axios";
const baseURL = "https://dev.mikulnews.com/api"; const baseURL = "https://mikulnews.com/api";
const axiosBaseInstance = axios.create({ const axiosBaseInstance = axios.create({
baseURL, baseURL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640" "X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
}, },
}); });

View File

@ -2,7 +2,7 @@ import axios from "axios";
import { postSignIn } from "../master-user"; import { postSignIn } from "../master-user";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
const baseURL = "https://dev.mikulnews.com/api"; const baseURL = "https://mikulnews.com/api";
const refreshToken = Cookies.get("refresh_token"); const refreshToken = Cookies.get("refresh_token");
@ -10,7 +10,7 @@ const axiosInterceptorInstance = axios.create({
baseURL, baseURL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640" "X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
}, },
withCredentials: true, withCredentials: true,
}); });
@ -28,7 +28,7 @@ axiosInterceptorInstance.interceptors.request.use(
}, },
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error);
} },
); );
// Response interceptor // Response interceptor
@ -66,7 +66,7 @@ axiosInterceptorInstance.interceptors.response.use(
} }
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default axiosInterceptorInstance; export default axiosInterceptorInstance;

View File

@ -94,16 +94,21 @@ export async function otpValidation(email: string, otpCode: string) {
return await httpPost(pathUrl, { email, otpCode }); return await httpPost(pathUrl, { email, otpCode });
} }
// export async function postArticleComment(data: any) {
// const headers = token
// ? {
// "content-type": "application/json",
// Authorization: `${token}`,
// }
// : {
// "content-type": "application/json",
// };
// return await httpPost(`/article-comments`, headers, data);
// }
export async function postArticleComment(data: any) { export async function postArticleComment(data: any) {
const headers = token const pathUrl = `/article-comments`;
? { return await httpPostInterceptor(pathUrl, data);
"content-type": "application/json",
Authorization: `Bearer ${token}`,
}
: {
"content-type": "application/json",
};
return await httpPost(`/article-comments`, headers, data);
} }
export async function editArticleComment(data: any, id: number) { export async function editArticleComment(data: any, id: number) {
@ -112,7 +117,7 @@ export async function editArticleComment(data: any, id: number) {
} }
export async function getArticleComment(id: string) { export async function getArticleComment(id: string) {
const pathUrl = `/article-comments?isPublic=true&articleId=${id}`; const pathUrl = `/article-comments?isPublic=false&articleId=${id}`;
return await httpGet(pathUrl); return await httpGet(pathUrl);
} }

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

View File

@ -310,6 +310,7 @@ export type PaginationRequest = {
category?: string; category?: string;
sortBy?: string; sortBy?: string;
sort?: string; sort?: string;
source?: string;
categorySlug?: string; categorySlug?: string;
isBanner?: boolean; isBanner?: boolean;
}; };

View File

@ -164,3 +164,11 @@ export function convertDateFormatNoTime(date: Date): string {
const day = `${date.getDate()}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0");
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
export function formatDate(date: Date | null) {
if (!date) return "";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}

View File

@ -2,31 +2,67 @@
* @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved. * @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/ */
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
import { Alignment } from '@ckeditor/ckeditor5-alignment'; import { Alignment } from "@ckeditor/ckeditor5-alignment";
import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; import { Autoformat } from "@ckeditor/ckeditor5-autoformat";
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles";
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; import { BlockQuote } from "@ckeditor/ckeditor5-block-quote";
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; import { CloudServices } from "@ckeditor/ckeditor5-cloud-services";
import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; import { CodeBlock } from "@ckeditor/ckeditor5-code-block";
import type { EditorConfig } from '@ckeditor/ckeditor5-core'; import type { EditorConfig } from "@ckeditor/ckeditor5-core";
import { Essentials } from '@ckeditor/ckeditor5-essentials'; import { Essentials } from "@ckeditor/ckeditor5-essentials";
import { FontSize } from '@ckeditor/ckeditor5-font'; import { FontSize } from "@ckeditor/ckeditor5-font";
import { Heading } from '@ckeditor/ckeditor5-heading'; import { Heading } from "@ckeditor/ckeditor5-heading";
import { Image, ImageCaption, ImageInsert, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image'; import {
import { Indent } from '@ckeditor/ckeditor5-indent'; Image,
import { Link } from '@ckeditor/ckeditor5-link'; ImageCaption,
import { List } from '@ckeditor/ckeditor5-list'; ImageInsert,
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; ImageStyle,
import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; ImageToolbar,
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office'; ImageUpload,
import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; } from "@ckeditor/ckeditor5-image";
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table'; import { Indent } from "@ckeditor/ckeditor5-indent";
import { TextTransformation } from '@ckeditor/ckeditor5-typing'; import { Link } from "@ckeditor/ckeditor5-link";
import { Undo } from '@ckeditor/ckeditor5-undo'; import { List } from "@ckeditor/ckeditor5-list";
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload'; import { MediaEmbed } from "@ckeditor/ckeditor5-media-embed";
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
import { PasteFromOffice } from "@ckeditor/ckeditor5-paste-from-office";
import { SourceEditing } from "@ckeditor/ckeditor5-source-editing";
import { Table, TableToolbar } from "@ckeditor/ckeditor5-table";
import { TextTransformation } from "@ckeditor/ckeditor5-typing";
import { Undo } from "@ckeditor/ckeditor5-undo";
import { SimpleUploadAdapter } from "@ckeditor/ckeditor5-upload";
declare class Editor extends ClassicEditor { declare class Editor extends ClassicEditor {
static builtinPlugins: (typeof Alignment | typeof Autoformat | typeof BlockQuote | typeof Bold | typeof CloudServices | typeof CodeBlock | typeof Essentials | typeof FontSize | typeof Heading | typeof Image | typeof ImageCaption | typeof ImageInsert | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof Indent | typeof Italic | typeof Link | typeof List | typeof MediaEmbed | typeof Paragraph | typeof PasteFromOffice | typeof SimpleUploadAdapter | typeof SourceEditing | typeof Table | typeof TableToolbar | typeof TextTransformation | typeof Undo)[]; static builtinPlugins: (
static defaultConfig: EditorConfig; | typeof Alignment
| typeof Autoformat
| typeof BlockQuote
| typeof Bold
| typeof CloudServices
| typeof CodeBlock
| typeof Essentials
| typeof FontSize
| typeof Heading
| typeof Image
| typeof ImageCaption
| typeof ImageInsert
| typeof ImageStyle
| typeof ImageToolbar
| typeof ImageUpload
| typeof Indent
| typeof Italic
| typeof Link
| typeof List
| typeof MediaEmbed
| typeof Paragraph
| typeof PasteFromOffice
| typeof SimpleUploadAdapter
| typeof SourceEditing
| typeof Table
| typeof TableToolbar
| typeof TextTransformation
| typeof Undo
)[];
static defaultConfig: EditorConfig;
} }
export default Editor; export default Editor;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
Changelog
=========
All changes in the package are documented in the main repository. See: https://github.com/ckeditor/ckeditor5/blob/master/CHANGELOG.md.
Changes for the past releases are available below.
## [19.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v18.0.0...v19.0.0) (April 29, 2020)
Internal changes only (updated dependencies, documentation, etc.).
## [18.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v17.0.0...v18.0.0) (March 19, 2020)
### Other changes
* Updated translations. ([f1beaaa](https://github.com/ckeditor/ckeditor5-alignment/commit/f1beaaa))
## [17.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v16.0.0...v17.0.0) (February 18, 2020)
### MAJOR BREAKING CHANGES
* The `align-left`, `align-right`, `align-center`, and `align-justify` icons have been moved to `@ckeditor/ckeditor5-core`.
### Other changes
* Moved alignment icons to `@ckeditor/ckeditor5-core` (see [ckeditor/ckeditor5-table#227](https://github.com/ckeditor/ckeditor5-table/issues/227)). ([410e279](https://github.com/ckeditor/ckeditor5-alignment/commit/410e279))
* Updated translations. ([288672f](https://github.com/ckeditor/ckeditor5-alignment/commit/288672f))
## [16.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v15.0.0...v16.0.0) (December 4, 2019)
### Other changes
* Updated translations. ([9085f7b](https://github.com/ckeditor/ckeditor5-alignment/commit/9085f7b))
## [15.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.2.0...v15.0.0) (October 23, 2019)
### Other changes
* Updated translations. ([a719974](https://github.com/ckeditor/ckeditor5-alignment/commit/a719974)) ([2fed077](https://github.com/ckeditor/ckeditor5-alignment/commit/2fed077))
* Added `pluginName` to the editor plugin part of the feature. ([3b42798](https://github.com/ckeditor/ckeditor5-alignment/commit/3b42798))
## [11.2.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.3...v11.2.0) (August 26, 2019)
### Features
* Integrated the text alignment feature with different editor content directions (LTR and RTL). See [ckeditor/ckeditor5#1151](https://github.com/ckeditor/ckeditor5/issues/1151). ([edc7d8b](https://github.com/ckeditor/ckeditor5-alignment/commit/edc7d8b))
### Bug fixes
* The UI buttons should be marked as toggleable for better assistive technologies support (see [ckeditor/ckeditor5#1403](https://github.com/ckeditor/ckeditor5/issues/1403)). ([599ea01](https://github.com/ckeditor/ckeditor5-alignment/commit/599ea01))
### Other changes
* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([54f81b3](https://github.com/ckeditor/ckeditor5-alignment/commit/54f81b3))
* The text alignment toolbar should have a proper `aria-label` attribute (see [ckeditor/ckeditor5#1404](https://github.com/ckeditor/ckeditor5/issues/1404)). ([3ed81de](https://github.com/ckeditor/ckeditor5-alignment/commit/3ed81de))
* Updated translations. ([feb4ab3](https://github.com/ckeditor/ckeditor5-alignment/commit/feb4ab3))
## [11.1.3](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.2...v11.1.3) (July 10, 2019)
Internal changes only (updated dependencies, documentation, etc.).
## [11.1.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.1...v11.1.2) (July 4, 2019)
### Other changes
* Updated translations. ([bb7f494](https://github.com/ckeditor/ckeditor5-alignment/commit/bb7f494))
## [11.1.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.0...v11.1.1) (June 6, 2019)
### Other changes
* Updated translations. ([32c32c1](https://github.com/ckeditor/ckeditor5-alignment/commit/32c32c1))
## [11.1.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.0.0...v11.1.0) (April 4, 2019)
### Features
* Marked alignment as a formatting attribute using the `AttributeProperties#isFormatting` property. Closes [ckeditor/ckeditor5#1664](https://github.com/ckeditor/ckeditor5/issues/1664). ([6358e08](https://github.com/ckeditor/ckeditor5-alignment/commit/6358e08))
### Other changes
* Updated translations. ([78bfc40](https://github.com/ckeditor/ckeditor5-alignment/commit/78bfc40))
## [11.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.4...v11.0.0) (February 28, 2019)
### Other changes
* Updated translations. ([45e8dd5](https://github.com/ckeditor/ckeditor5-alignment/commit/45e8dd5)) ([a92c37b](https://github.com/ckeditor/ckeditor5-alignment/commit/a92c37b)) ([ef68e54](https://github.com/ckeditor/ckeditor5-alignment/commit/ef68e54))
### BREAKING CHANGES
* Upgraded minimal versions of Node to `8.0.0` and npm to `5.7.1`. See: [ckeditor/ckeditor5#1507](https://github.com/ckeditor/ckeditor5/issues/1507). ([612ea3c](https://github.com/ckeditor/ckeditor5-cloud-services/commit/612ea3c))
## [10.0.4](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.3...v10.0.4) (December 5, 2018)
### Other changes
* Improved SVG icons size. See [ckeditor/ckeditor5-theme-lark#206](https://github.com/ckeditor/ckeditor5-theme-lark/issues/206). ([1d71d33](https://github.com/ckeditor/ckeditor5-alignment/commit/1d71d33))
* Updated translations. ([547f8d8](https://github.com/ckeditor/ckeditor5-alignment/commit/547f8d8)) ([43d8225](https://github.com/ckeditor/ckeditor5-alignment/commit/43d8225))
## [10.0.3](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.2...v10.0.3) (October 8, 2018)
### Other changes
* Updated translations. ([5b30202](https://github.com/ckeditor/ckeditor5-alignment/commit/5b30202))
## [10.0.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.1...v10.0.2) (July 18, 2018)
### Other changes
* Updated translations. ([33c281c](https://github.com/ckeditor/ckeditor5-alignment/commit/33c281c))
## [10.0.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.0...v10.0.1) (June 21, 2018)
### Other changes
* Updated translations.
## [10.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.4...v10.0.0) (April 25, 2018)
### Other changes
* Changed the license to GPL2+ only. See [ckeditor/ckeditor5#991](https://github.com/ckeditor/ckeditor5/issues/991). ([eed1029](https://github.com/ckeditor/ckeditor5-alignment/commit/eed1029))
* Updated translations. ([baa1fbe](https://github.com/ckeditor/ckeditor5-alignment/commit/baa1fbe))
### BREAKING CHANGES
* The license under which CKEditor&nbsp;5 is released has been changed from a triple GPL, LGPL and MPL license to a GPL2+ only. See [ckeditor/ckeditor5#991](https://github.com/ckeditor/ckeditor5/issues/991) for more information.
## [1.0.0-beta.4](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.2...v1.0.0-beta.4) (April 19, 2018)
### Other changes
* Updated translations. ([586ae62](https://github.com/ckeditor/ckeditor5-alignment/commit/586ae62))
## [1.0.0-beta.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.1...v1.0.0-beta.2) (April 10, 2018)
Internal changes only (updated dependencies, documentation, etc.).
## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v0.0.1...v1.0.0-beta.1) (March 15, 2018)
### Features
* Initial implementation. Closes [#2](https://github.com/ckeditor/ckeditor5-alignment/issues/2).

View File

@ -0,0 +1,17 @@
Software License Agreement
==========================
**CKEditor&nbsp;5 text alignment feature** https://github.com/ckeditor/ckeditor5-alignment <br>
Copyright (c) 20032024, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
Sources of Intellectual Property Included in CKEditor
-----------------------------------------------------
Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
Trademarks
----------
**CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks, or service marks of their respective holders.

View File

@ -0,0 +1,20 @@
CKEditor&nbsp;5 text alignment feature
========================================
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-alignment.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-alignment)
[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
[![Build Status](https://travis-ci.com/ckeditor/ckeditor5.svg?branch=master)](https://app.travis-ci.com/github/ckeditor/ckeditor5)
This package implements text alignment support for CKEditor&nbsp;5.
## Demo
Check out the [demo in the text alignment feature guide](https://ckeditor.com/docs/ckeditor5/latest/features/text-alignment.html#demo).
## Documentation
See the [`@ckeditor/ckeditor5-alignment` package](https://ckeditor.com/docs/ckeditor5/latest/api/alignment.html) page in [CKEditor&nbsp;5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).
## License
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(n){const e=n.af=n.af||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs",Justify:"Belyn beide kante","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.ar=n.ar||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"محاذاة في المنتصف","Align left":"محاذاة لليسار","Align right":"محاذاة لليمين",Justify:"ضبط","Text alignment":"محاذاة النص","Text alignment toolbar":"شريط أدوات محاذاة النص"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.az=n.az||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Mərkəzə düzləndir","Align left":"Soldan düzləndir","Align right":"Sağdan düzləndir",Justify:"Eninə görə","Text alignment":"Mətn düzləndirməsi","Text alignment toolbar":"Mətnin düzləndirmə paneli"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.bg=n.bg||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Централно подравняване","Align left":"Ляво подравняване","Align right":"Дясно подравняване",Justify:"Разпредели по равно","Text alignment":"Подравняване на текста","Text alignment toolbar":"Лента за подравняване на текст"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.bn=n.bn||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"কেন্দ্র সারিবদ্ধ করুন","Align left":"বামে সারিবদ্ধ করুন","Align right":"ডানদিকে সারিবদ্ধ করুন",Justify:"জাস্টিফাই","Text alignment":"টেক্সট সারিবদ্ধকরণ","Text alignment toolbar":"টেক্সট শ্রেণীবিন্যাস টুলবার"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const a=n.bs=n.bs||{};a.dictionary=Object.assign(a.dictionary||{},{"Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Justify:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(i){const e=i.ca=i.ca||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Alineació centre","Align left":"Alineació esquerra","Align right":"Alineació dreta",Justify:"Justificar","Text alignment":"Alineació text","Text alignment toolbar":"Barra d'eines d'alineació de text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const t=n.cs=n.cs||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Zarovnat na střed","Align left":"Zarovnat vlevo","Align right":"Zarovnat vpravo",Justify:"Zarovnat do bloku","Text alignment":"Zarovnání textu","Text alignment toolbar":"Panel nástrojů zarovnání textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(t){const n=t.da=t.da||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Justér center","Align left":"Justér venstre","Align right":"Justér højre",Justify:"Justér","Text alignment":"Tekstjustering","Text alignment toolbar":"Tekstjustering værktøjslinje"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(t){const i=t["de-ch"]=t["de-ch"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Zentriert","Align left":"Linksbündig","Align right":"Rechtsbündig",Justify:"Blocksatz","Text alignment":"Textausrichtung","Text alignment toolbar":"Textausrichtung Werkzeugleiste"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const t=n.de=n.de||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Zentriert","Align left":"Linksbündig","Align right":"Rechtsbündig",Justify:"Blocksatz","Text alignment":"Textausrichtung","Text alignment toolbar":"Text-Ausrichtung Toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.el=n.el||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Στοίχιση στο κέντρο","Align left":"Στοίχιση αριστερά","Align right":"Στοίχιση δεξιά",Justify:"Πλήρης στοίχηση","Text alignment":"Στοίχιση κειμένου","Text alignment toolbar":"Γραμμή εργαλείων στοίχισης κειμένου"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const t=n["en-au"]=n["en-au"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Align centre","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":"Text alignment toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n["en-gb"]=n["en-gb"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Align center","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":""})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(i){const e=i["es-co"]=i["es-co"]||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Centrar","Align left":"Alinear a la izquierda","Align right":"Alinear a la derecha",Justify:"Justificar","Text alignment":"Alineación de texto","Text alignment toolbar":"Herramientas de alineación de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(e){const i=e.es=e.es||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Centrar","Align left":"Alinear a la izquierda","Align right":"Alinear a la derecha",Justify:"Justificar","Text alignment":"Alineación del texto","Text alignment toolbar":"Barra de herramientas de alineación del texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.et=n.et||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Keskjoondus","Align left":"Vasakjoondus","Align right":"Paremjoondus",Justify:"Rööpjoondus","Text alignment":"Teksti joondamine","Text alignment toolbar":"Teksti joonduse tööriistariba"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.fa=n.fa||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"تراز وسط","Align left":"تراز چپ","Align right":"تراز راست",Justify:"هم تراز کردن","Text alignment":"تراز متن","Text alignment toolbar":"نوار ابزار ترازبندی متن"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(a){const i=a.fi=a.fi||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Tasaa keskelle","Align left":"Tasaa vasemmalle","Align right":"Tasaa oikealle",Justify:"Tasaa molemmat reunat","Text alignment":"Tekstin tasaus","Text alignment toolbar":"Tekstin suuntauksen työkalupalkki"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(e){const t=e.fr=e.fr||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Centrer","Align left":"Aligner à gauche","Align right":"Aligner à droite",Justify:"Justifier","Text alignment":"Alignement du texte","Text alignment toolbar":"Barre d'outils d'alignement du texte"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(t){const e=t.gl=t.gl||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Centrar horizontalmente","Align left":"Aliñar á esquerda","Align right":"Aliñar á dereita",Justify:"Xustificado","Text alignment":"Aliñamento do texto","Text alignment toolbar":"Barra de ferramentas de aliñamento de textos"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.he=n.he||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"יישור באמצע","Align left":"יישור לשמאל","Align right":"יישור לימין",Justify:"מרכוז גבולות","Text alignment":"יישור טקסט","Text alignment toolbar":"סרגל כלים יישור טקסט"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(i){const n=i.hi=i.hi||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Align center","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":"Text alignment toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const a=n.hr=n.hr||{};a.dictionary=Object.assign(a.dictionary||{},{"Align center":"Poravnaj po sredini","Align left":"Poravnaj ulijevo","Align right":"Poravnaj udesno",Justify:"Razvuci","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(i){const t=i.hu=i.hu||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Középre igazítás","Align left":"Balra igazítás","Align right":"Jobbra igazítás",Justify:"Sorkizárt","Text alignment":"Szöveg igazítása","Text alignment toolbar":"Szöveg igazítás eszköztár"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(a){const t=a.id=a.id||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Rata tengah","Align left":"Rata kiri","Align right":"Rata kanan",Justify:"Rata kanan-kiri","Text alignment":"Perataan teks","Text alignment toolbar":"Alat perataan teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(i){const n=i.it=i.it||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Allinea al centro","Align left":"Allinea a sinistra","Align right":"Allinea a destra",Justify:"Giustifica","Text alignment":"Allineamento del testo","Text alignment toolbar":"Barra degli strumenti dell'allineamento"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.ja=n.ja||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"中央揃え","Align left":"左揃え","Align right":"右揃え",Justify:"両端揃え","Text alignment":"文字揃え","Text alignment toolbar":"テキストの整列"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const t=n.jv=n.jv||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Rata tengah","Align left":"Rata kiwa","Align right":"Rata tengen",Justify:"Rata kiwa tengen","Text alignment":"Perataan seratan","Text alignment toolbar":""})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.kk=n.kk||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Ортадан туралау","Align left":"Солға туралау","Align right":"Оңға туралау",Justify:"","Text alignment":"Мәтінді туралау","Text alignment toolbar":"Мәтінді туралау құралдар тақтасы"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
!function(n){const i=n.km=n.km||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"តម្រឹម​កណ្ដាល","Align left":"តម្រឹម​ឆ្វេង","Align right":"តម្រឹម​ស្ដាំ",Justify:"តម្រឹម​សងខាង","Text alignment":"ការ​តម្រឹម​អក្សរ","Text alignment toolbar":"របារ​ឧបករណ៍​តម្រឹម​អក្សរ"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

Some files were not shown because too many files have changed in this diff Show More