Compare commits
No commits in common. "main" and "main-mirror" have entirely different histories.
main
...
main-mirro
42
.drone.yml
42
.drone.yml
|
|
@ -1,42 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -7,16 +7,15 @@ build-dev:
|
||||||
when: on_success
|
when: on_success
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
image:
|
image: docker:stable
|
||||||
name: docker:25.0.3-cli
|
|
||||||
services:
|
services:
|
||||||
- name: docker:25.0.3-dind
|
- name: docker:dind
|
||||||
command: ["--insecure-registry=38.47.185.86:8900"]
|
command: ["--insecure-registry=103.82.242.92:8900"]
|
||||||
script:
|
script:
|
||||||
- docker logout
|
- docker logout
|
||||||
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
|
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
|
||||||
- docker build -t 38.47.185.86:8900/medols/web-mikul-news:dev .
|
- docker build -t 103.82.242.92:8900/medols/web-mikul-news:dev .
|
||||||
- docker push 38.47.185.86:8900/medols/web-mikul-news:dev
|
- docker push 103.82.242.92:8900/medols/web-mikul-news:dev
|
||||||
|
|
||||||
auto-deploy:
|
auto-deploy:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|
@ -27,4 +26,4 @@ auto-deploy:
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
script:
|
script:
|
||||||
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-mikul-news/build?token=autodeploymedols
|
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-mikul-news/build?token=autodeploymedols
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Author from "@/components/landing-page/author";
|
import Author from "@/components/landing-page/author";
|
||||||
import CitizenNews from "@/components/landing-page/citizen-news/citizen-news";
|
|
||||||
import HeaderCitizen from "@/components/landing-page/citizen-news/header-citizen";
|
import HeaderCitizen from "@/components/landing-page/citizen-news/header-citizen";
|
||||||
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";
|
||||||
|
|
@ -12,7 +11,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,13 +20,12 @@ 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 />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<HeaderCitizen />
|
<HeaderCitizen />
|
||||||
<CitizenNews />
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import Author from "@/components/landing-page/author";
|
import Author from "@/components/landing-page/author";
|
||||||
import DevelopmentNews from "@/components/landing-page/development/development-news";
|
|
||||||
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 HealthNews from "@/components/landing-page/health/health-news";
|
|
||||||
import HeaderHealth from "@/components/landing-page/health/header-health";
|
import HeaderHealth from "@/components/landing-page/health/header-health";
|
||||||
import Latest from "@/components/landing-page/latest";
|
import Latest from "@/components/landing-page/latest";
|
||||||
import LatestandPopular from "@/components/landing-page/latest-and-popular";
|
import LatestandPopular from "@/components/landing-page/latest-and-popular";
|
||||||
|
|
@ -13,7 +11,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,13 +20,12 @@ 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 />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<HeaderHealth />
|
<HeaderHealth />
|
||||||
<HealthNews />
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
33
app/page.tsx
33
app/page.tsx
|
|
@ -1,26 +1,39 @@
|
||||||
|
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 LatestNews from "@/components/landing-page/latest-news";
|
import News from "@/components/landing-page/news";
|
||||||
import Development from "@/components/landing-page/development";
|
import { id } from "date-fns/locale";
|
||||||
import OpinionNews from "@/components/landing-page/opinion-news";
|
import Image from "next/image";
|
||||||
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">
|
||||||
|
<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 />
|
<Navbar />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Header />
|
<Header />
|
||||||
</div>
|
</div>
|
||||||
<LatestNews />
|
<Latest id={2} />
|
||||||
<Development />
|
<News />
|
||||||
<OpinionNews />
|
<Author />
|
||||||
<NewsTerkini />
|
<LatestandPopular />
|
||||||
<YouTubeSection />
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,25 @@ 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 {
|
import { getArticleById, getListArticle } from "@/service/article";
|
||||||
getArticleById,
|
import { close, loading } from "@/config/swal";
|
||||||
getArticleBySlug,
|
import { useParams } from "next/navigation";
|
||||||
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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -53,20 +32,9 @@ 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[]>([]);
|
||||||
|
|
@ -79,82 +47,18 @@ 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" },
|
||||||
|
|
@ -162,35 +66,6 @@ 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]);
|
||||||
|
|
@ -205,6 +80,7 @@ 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") {
|
||||||
|
|
@ -213,6 +89,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -303,49 +180,17 @@ export default function DetailContent() {
|
||||||
initStateData();
|
initStateData();
|
||||||
}, [listCategory]);
|
}, [listCategory]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedIndex(0);
|
|
||||||
}, [detailFiles]);
|
|
||||||
|
|
||||||
async function initStateData() {
|
async function initStateData() {
|
||||||
loading();
|
loading();
|
||||||
try {
|
const res = await getArticleById(id);
|
||||||
// 1️⃣ Ambil artikel by slug
|
const data = res.data?.data;
|
||||||
const res = await getArticleBySlug(slug);
|
|
||||||
const data = res?.data?.data;
|
|
||||||
|
|
||||||
if (!data?.id) return;
|
setThumbnail(data?.thumbnailUrl);
|
||||||
|
setDiseId(data?.aiArticleId);
|
||||||
setArticleDetail(data);
|
setDetailFiles(data?.files);
|
||||||
setThumbnail(data.thumbnailUrl);
|
setArticleDetail(data); // <-- Add this
|
||||||
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();
|
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 (
|
||||||
<>
|
<>
|
||||||
|
|
@ -387,69 +232,39 @@ export default function DetailContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-[#31942E] font-medium">
|
<span className="text-[#31942E] font-medium">
|
||||||
{articleDetail?.customCreatorName || articleDetail?.createdByName}
|
{articleDetail?.createdByName}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(articleDetail?.publishedAt ?? articleDetail?.createdAt)
|
<span>
|
||||||
.toLocaleString("id-ID", {
|
{new Date(articleDetail?.createdAt).toLocaleDateString(
|
||||||
|
"id-ID",
|
||||||
|
{
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
}
|
||||||
minute: "2-digit",
|
)}
|
||||||
hour12: false,
|
</span>
|
||||||
timeZone: "Asia/Jakarta",
|
|
||||||
})
|
|
||||||
.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">
|
||||||
{/* Gambar utama */}
|
{articleDetail?.files?.[0]?.file_url ? (
|
||||||
{detailFiles.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<Image
|
<Image
|
||||||
src={detailFiles[selectedIndex]?.fileUrl}
|
src={articleDetail.files[0].file_url}
|
||||||
alt={detailFiles[selectedIndex]?.fileAlt || "Berita"}
|
alt="Berita"
|
||||||
width={800}
|
width={800}
|
||||||
height={400}
|
height={400}
|
||||||
className="rounded-lg w-full object-cover"
|
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="h-[400px] flex items-center justify-center bg-gray-100 rounded-lg">
|
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
|
||||||
<p className="text-gray-400">Gambar tidak tersedia</p>
|
<p className="text-gray-400 text-sm">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>
|
||||||
|
|
@ -521,75 +336,23 @@ export default function DetailContent() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="prose max-w-none text-justify">
|
<p className="text-gray-700 leading-relaxed text-justify">
|
||||||
<div
|
<span className="text-black font-bold text-md">
|
||||||
dangerouslySetInnerHTML={removeImgTags(
|
Mikulnews.com -
|
||||||
formatTextToHtmlTag(articleDetail?.htmlDescription),
|
</span>
|
||||||
)}
|
|
||||||
className="text-sm lg:text-xl lg:leading-8 text-justify space-y-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <Author /> */}
|
{articleDetail?.description}
|
||||||
<div className="w-full bg-white py-6">
|
</p>
|
||||||
<p className="mx-10 text-2xl mb-4 ">AUTHOR</p>
|
<Author />
|
||||||
<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 ? (
|
||||||
articleDetail.tags
|
<span className="bg-gray-100 text-gray-700 text-sm px-2 py-1 rounded">
|
||||||
.split(",") // pisahkan berdasarkan koma
|
{articleDetail.tags}
|
||||||
.map((tag: string, index: number) => (
|
</span>
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
@ -597,16 +360,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"
|
||||||
|
|
@ -619,41 +382,15 @@ 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>
|
|
||||||
<h3 className="text-lg font-semibold border-b pb-2">Komentar</h3>
|
<form className="space-y-6 mt-6">
|
||||||
{commentList?.length === 0 ? (
|
|
||||||
<p className="text-sm text-gray-500">Belum ada komentar.</p>
|
|
||||||
) : (
|
|
||||||
commentList?.map((comment: any) => (
|
|
||||||
<div
|
|
||||||
key={comment?.id}
|
|
||||||
className="border rounded-lg p-3 bg-gray-50 shadow-sm"
|
|
||||||
>
|
|
||||||
<p className="text-sm text-gray-800 whitespace-pre-line">
|
|
||||||
{comment?.message}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
{comment?.commentFromName || "Anonim"} •{" "}
|
|
||||||
{new Date(comment?.createdAt).toLocaleString("id-ID", {
|
|
||||||
dateStyle: "short",
|
|
||||||
timeStyle: "short",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<form className="space-y-6 mt-6" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
{!needOtp ? (
|
|
||||||
<>
|
|
||||||
{/* Komentar */}
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="komentar"
|
htmlFor="komentar"
|
||||||
|
|
@ -664,11 +401,10 @@ export default function DetailContent() {
|
||||||
<textarea
|
<textarea
|
||||||
id="komentar"
|
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"
|
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 })}
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nama */}
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="nama"
|
htmlFor="nama"
|
||||||
|
|
@ -680,11 +416,11 @@ export default function DetailContent() {
|
||||||
type="text"
|
type="text"
|
||||||
id="nama"
|
id="nama"
|
||||||
className="w-full border border-gray-300 rounded-md p-2"
|
className="w-full border border-gray-300 rounded-md p-2"
|
||||||
{...register("name", { required: true })}
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
|
|
@ -696,51 +432,43 @@ export default function DetailContent() {
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
className="w-full border border-gray-300 rounded-md p-2"
|
className="w-full border border-gray-300 rounded-md p-2"
|
||||||
{...register("email", { required: true })}
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="website"
|
||||||
|
className="block text-sm font-medium mb-1"
|
||||||
|
>
|
||||||
|
Situs Web
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="website"
|
||||||
|
className="w-full border border-gray-300 rounded-md p-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-2 mt-2">
|
||||||
|
<input type="checkbox" id="saveInfo" className="mt-1" />
|
||||||
|
<label htmlFor="saveInfo" className="text-sm text-gray-700">
|
||||||
|
Simpan nama, email, dan situs web saya pada peramban ini untuk
|
||||||
|
komentar saya berikutnya.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-red-600 text-sm">
|
||||||
|
The reCAPTCHA verification period has expired. Please reload the
|
||||||
|
page.
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
className="bg-green-600 hover:bg-green-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2"
|
||||||
>
|
>
|
||||||
KIRIM KOMENTAR
|
KIRIM KOMENTAR
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Kode verifikasi sudah dikirimkan. Silakan cek Email Anda!
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1 mt-4">
|
|
||||||
OTP
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2 justify-center">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<input
|
|
||||||
key={i}
|
|
||||||
type="text"
|
|
||||||
maxLength={1}
|
|
||||||
className="w-10 h-10 text-center border border-gray-300 rounded-md text-lg"
|
|
||||||
value={otpValue[i] || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newValue = otpValue.split("");
|
|
||||||
newValue[i] = e.target.value.replace(/[^0-9]/g, "");
|
|
||||||
setOtpValue(newValue.join(""));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|
@ -748,32 +476,13 @@ 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">
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block w-full"
|
|
||||||
>
|
|
||||||
<div className="relative w-full h-[350px] flex justify-center">
|
|
||||||
<Image
|
<Image
|
||||||
src={bannerAd.contentFileUrl}
|
src={"/kolom.png"}
|
||||||
alt={bannerAd.title || "Iklan Banner"}
|
alt="Iklan"
|
||||||
width={1200} // ukuran dasar untuk responsive
|
width={345}
|
||||||
height={350}
|
height={345}
|
||||||
className="object-cover w-full h-full"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,14 +1,11 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
// components/custom-editor.js
|
||||||
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
|
||||||
|
|
||||||
import "@/styles/custom-editor.css";
|
import React from "react";
|
||||||
|
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function CustomEditor(props) {
|
function CustomEditor(props) {
|
||||||
const maxHeight = props.maxHeight || 600;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ckeditor-wrapper">
|
|
||||||
<CKEditor
|
<CKEditor
|
||||||
editor={Editor}
|
editor={Editor}
|
||||||
data={props.initialData}
|
data={props.initialData}
|
||||||
|
|
@ -36,43 +33,8 @@ function CustomEditor(props) {
|
||||||
"codeBlock",
|
"codeBlock",
|
||||||
"sourceEditing",
|
"sourceEditing",
|
||||||
],
|
],
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #111 !important;
|
|
||||||
background: #fff !important;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5em 0 !important;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 1em 0 0.5em 0;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
ul, ol {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
border-left: 4px solid #d1d5db;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
height: props.height || 400,
|
|
||||||
removePlugins: ["Title"],
|
|
||||||
mobile: {
|
|
||||||
theme: "silver",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
// Import the optimized editor (choose one based on your migration)
|
|
||||||
// import OptimizedEditor from './optimized-editor'; // TinyMCE
|
|
||||||
// import OptimizedCKEditor from './optimized-ckeditor'; // CKEditor5 Classic
|
|
||||||
// import MinimalEditor from './minimal-editor'; // React Quill
|
|
||||||
|
|
||||||
interface EditorExampleProps {
|
|
||||||
editorType?: 'tinymce' | 'ckeditor' | 'quill';
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditorExample: React.FC<EditorExampleProps> = ({
|
|
||||||
editorType = 'tinymce'
|
|
||||||
}) => {
|
|
||||||
const [content, setContent] = useState('<p>Hello, this is the editor content!</p>');
|
|
||||||
const [savedContent, setSavedContent] = useState('');
|
|
||||||
|
|
||||||
const handleContentChange = (newContent: string) => {
|
|
||||||
setContent(newContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
setSavedContent(content);
|
|
||||||
console.log('Content saved:', content);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setContent('<p>Content has been reset!</p>');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Rich Text Editor Example</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
This is an optimized editor with {editorType} - much smaller bundle size and better performance!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Editor Panel */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold">Editor</h3>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleReset}
|
|
||||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg">
|
|
||||||
{/* Choose your editor based on migration */}
|
|
||||||
{editorType === 'tinymce' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
TinyMCE Editor (200KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <OptimizedEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">TinyMCE Editor Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editorType === 'ckeditor' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
CKEditor5 Classic (800KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <OptimizedCKEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">CKEditor5 Classic Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editorType === 'quill' && (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-gray-500 text-sm mb-2">
|
|
||||||
React Quill (100KB bundle)
|
|
||||||
</p>
|
|
||||||
{/* <MinimalEditor
|
|
||||||
initialData={content}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
height={400}
|
|
||||||
placeholder="Start typing your content..."
|
|
||||||
/> */}
|
|
||||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
|
||||||
<p className="text-gray-500">React Quill Component</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preview Panel */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold">Preview</h3>
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Current Content:</h4>
|
|
||||||
<div
|
|
||||||
className="prose max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{savedContent && (
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Saved Content:</h4>
|
|
||||||
<div
|
|
||||||
className="prose max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: savedContent }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<h4 className="font-medium mb-2">Raw HTML:</h4>
|
|
||||||
<pre className="text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">
|
|
||||||
{content}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Performance Info */}
|
|
||||||
<div className="mt-8 p-4 bg-blue-50 rounded-lg">
|
|
||||||
<h4 className="font-medium text-blue-900 mb-2">Performance Benefits:</h4>
|
|
||||||
<ul className="text-sm text-blue-800 space-y-1">
|
|
||||||
<li>• 90% smaller bundle size compared to custom CKEditor5</li>
|
|
||||||
<li>• Faster initial load time</li>
|
|
||||||
<li>• Better mobile performance</li>
|
|
||||||
<li>• Reduced memory usage</li>
|
|
||||||
<li>• Improved Lighthouse score</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditorExample;
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import CustomEditor from './custom-editor';
|
|
||||||
import FormEditor from './form-editor';
|
|
||||||
|
|
||||||
export default function EditorTest() {
|
|
||||||
const [testData, setTestData] = useState('Initial test content');
|
|
||||||
const [editorType, setEditorType] = useState('custom');
|
|
||||||
|
|
||||||
const { control, setValue, watch, handleSubmit } = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
title: 'Test Title',
|
|
||||||
description: testData,
|
|
||||||
creatorName: 'Test Creator'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const watchedValues = watch();
|
|
||||||
|
|
||||||
const handleSetValue = () => {
|
|
||||||
const newContent = `<p>Updated content at ${new Date().toLocaleTimeString()}</p><p>This content was set via setValue</p>`;
|
|
||||||
setValue('description', newContent);
|
|
||||||
setTestData(newContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetEmpty = () => {
|
|
||||||
setValue('description', '');
|
|
||||||
setTestData('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetHTML = () => {
|
|
||||||
const htmlContent = `
|
|
||||||
<h2>HTML Content Test</h2>
|
|
||||||
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
|
|
||||||
<ul>
|
|
||||||
<li>List item 1</li>
|
|
||||||
<li>List item 2</li>
|
|
||||||
<li>List item 3</li>
|
|
||||||
</ul>
|
|
||||||
<p>Updated at: ${new Date().toLocaleTimeString()}</p>
|
|
||||||
`;
|
|
||||||
setValue('description', htmlContent);
|
|
||||||
setTestData(htmlContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
console.log('Form submitted:', data);
|
|
||||||
alert('Form submitted! Check console for data.');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
|
||||||
<h1 className="text-2xl font-bold">Editor Test Component</h1>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>Editor Type:</Label>
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<Button
|
|
||||||
variant={editorType === 'custom' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setEditorType('custom')}
|
|
||||||
>
|
|
||||||
CustomEditor
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={editorType === 'form' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setEditorType('form')}
|
|
||||||
>
|
|
||||||
FormEditor
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button onClick={handleSetValue} variant="outline">
|
|
||||||
Set Value (Current Time)
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSetEmpty} variant="outline">
|
|
||||||
Set Empty
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSetHTML} variant="outline">
|
|
||||||
Set HTML Content
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>Current Test Data:</Label>
|
|
||||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
|
||||||
{testData || '(empty)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Watched Form Values:</Label>
|
|
||||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
|
||||||
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>Title:</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input {...field} className="mt-1" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Description (Editor):</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
editorType === 'custom' ? (
|
|
||||||
<CustomEditor
|
|
||||||
onChange={field.onChange}
|
|
||||||
initialData={field.value}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormEditor
|
|
||||||
onChange={field.onChange}
|
|
||||||
initialData={field.value}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Creator Name:</Label>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="creatorName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input {...field} className="mt-1" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Submit Form
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<h3 className="font-semibold mb-2">Instructions:</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
|
||||||
<li>Switch between CustomEditor and FormEditor to test both</li>
|
|
||||||
<li>Click "Set Value" to test setValue functionality</li>
|
|
||||||
<li>Click "Set Empty" to test empty content handling</li>
|
|
||||||
<li>Click "Set HTML Content" to test rich HTML content</li>
|
|
||||||
<li>Type in the editor to test onChange functionality</li>
|
|
||||||
<li>Submit the form to see all data</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
|
@ -1,102 +0,0 @@
|
||||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function FormEditor({ onChange, initialData }) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
|
||||||
const [editorContent, setEditorContent] = useState(initialData || "");
|
|
||||||
|
|
||||||
// Handle editor initialization
|
|
||||||
const handleInit = useCallback((evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
setIsEditorReady(true);
|
|
||||||
|
|
||||||
// Set initial content when editor is ready
|
|
||||||
if (editorContent) {
|
|
||||||
editor.setContent(editorContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle content changes
|
|
||||||
editor.on('change', () => {
|
|
||||||
const content = editor.getContent();
|
|
||||||
setEditorContent(content);
|
|
||||||
if (onChange) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [editorContent, onChange]);
|
|
||||||
|
|
||||||
// Watch for initialData changes (from setValue)
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData !== editorContent) {
|
|
||||||
setEditorContent(initialData || "");
|
|
||||||
|
|
||||||
// Update editor content if ready
|
|
||||||
if (editorRef.current && isEditorReady) {
|
|
||||||
editorRef.current.setContent(initialData || "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [initialData, editorContent, isEditorReady]);
|
|
||||||
|
|
||||||
// Handle initial data when editor becomes ready
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditorReady && editorContent && editorRef.current) {
|
|
||||||
editorRef.current.setContent(editorContent);
|
|
||||||
}
|
|
||||||
}, [isEditorReady, editorContent]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
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',
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FormEditor;
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
// components/minimal-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function MinimalEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple onChange handler - no debouncing, no complex logic
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (props.onChange) {
|
|
||||||
props.onChange(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Minimal settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Basic content handling
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MinimalEditor;
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
interface OptimizedEditorProps {
|
|
||||||
initialData?: string;
|
|
||||||
onChange?: (data: string) => void;
|
|
||||||
height?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
readOnly?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
|
||||||
initialData = "",
|
|
||||||
onChange,
|
|
||||||
height = 400,
|
|
||||||
placeholder = "Start typing...",
|
|
||||||
disabled = false,
|
|
||||||
readOnly = false,
|
|
||||||
}) => {
|
|
||||||
const editorRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const handleEditorChange = (content: string) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInit = (evt: any, editor: any) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={initialData}
|
|
||||||
onEditorChange={handleEditorChange}
|
|
||||||
disabled={disabled}
|
|
||||||
init={{
|
|
||||||
height,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
"advlist",
|
|
||||||
"autolink",
|
|
||||||
"lists",
|
|
||||||
"link",
|
|
||||||
"image",
|
|
||||||
"charmap",
|
|
||||||
"preview",
|
|
||||||
"anchor",
|
|
||||||
"searchreplace",
|
|
||||||
"visualblocks",
|
|
||||||
"code",
|
|
||||||
"fullscreen",
|
|
||||||
"insertdatetime",
|
|
||||||
"media",
|
|
||||||
"table",
|
|
||||||
"code",
|
|
||||||
"help",
|
|
||||||
"wordcount",
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
"undo redo | blocks | " +
|
|
||||||
"bold italic forecolor | alignleft aligncenter " +
|
|
||||||
"alignright alignjustify | bullist numlist outdent indent | " +
|
|
||||||
"removeformat | table | code | help",
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${height - 32}px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder,
|
|
||||||
readonly: readOnly,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Performance optimizations
|
|
||||||
cache_suffix: "?v=1.0",
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
// Auto-save feature
|
|
||||||
auto_save: true,
|
|
||||||
auto_save_interval: "30s",
|
|
||||||
// Better mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: "silver",
|
|
||||||
plugins: ["lists", "autolink", "link", "image", "table"],
|
|
||||||
toolbar: "bold italic | bullist numlist | link image",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptimizedEditor;
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
// components/readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function ReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable all editing capabilities
|
|
||||||
editor.on('keydown keyup keypress input', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('paste', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update content when props change
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorRef.current && props.initialData) {
|
|
||||||
editorRef.current.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
}, [props.initialData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false, // No toolbar for read-only mode
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Minimal settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Performance optimizations for read-only
|
|
||||||
cache_suffix: '?v=1.0',
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
// Disable editing features
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
paste_word_valid_elements: false,
|
|
||||||
paste_retain_style_properties: false,
|
|
||||||
// Additional read-only settings
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
// Disable all editing
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: false
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReadOnlyEditor;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
// components/simple-editor.js
|
|
||||||
|
|
||||||
import React, { useRef, useState, useCallback } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function SimpleEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const [editorInstance, setEditorInstance] = useState(null);
|
|
||||||
|
|
||||||
const handleInit = useCallback((evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
setEditorInstance(editor);
|
|
||||||
|
|
||||||
// Set initial content
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable automatic content updates
|
|
||||||
editor.settings.auto_focus = false;
|
|
||||||
editor.settings.forced_root_block = 'p';
|
|
||||||
|
|
||||||
// Store the onChange callback
|
|
||||||
editor.onChangeCallback = props.onChange;
|
|
||||||
|
|
||||||
// Handle content changes without triggering re-renders
|
|
||||||
editor.on('change keyup input', (e) => {
|
|
||||||
if (editor.onChangeCallback) {
|
|
||||||
const content = editor.getContent();
|
|
||||||
editor.onChangeCallback(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}, [props.initialData, props.onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Better content handling
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
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',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimpleEditor;
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
// components/simple-readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function SimpleReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Disable all editing capabilities
|
|
||||||
editor.on('keydown keyup keypress input', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('paste', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimpleReadOnlyEditor;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StableEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const onChangeRef = useRef(props.onChange);
|
|
||||||
|
|
||||||
// Update onChange ref when props change
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeRef.current = props.onChange;
|
|
||||||
}, [props.onChange]);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a simple change handler that doesn't trigger re-renders
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (onChangeRef.current) {
|
|
||||||
onChangeRef.current(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings for stability
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable all problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Content handling
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
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',
|
|
||||||
// Prevent automatic updates
|
|
||||||
element_format: 'html',
|
|
||||||
valid_children: '+body[style]',
|
|
||||||
extended_valid_elements: 'span[*]',
|
|
||||||
custom_elements: '~span',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StableEditor;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import React, { useRef, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StaticEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
const onChangeRef = useRef(props.onChange);
|
|
||||||
|
|
||||||
// Update onChange ref when props change
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeRef.current = props.onChange;
|
|
||||||
}, [props.onChange]);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Set initial content if provided
|
|
||||||
if (props.initialData) {
|
|
||||||
editor.setContent(props.initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a simple change handler that doesn't trigger re-renders
|
|
||||||
editor.on('change', () => {
|
|
||||||
if (onChangeRef.current) {
|
|
||||||
onChangeRef.current(editor.getContent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: 400,
|
|
||||||
menubar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
|
||||||
],
|
|
||||||
toolbar: 'undo redo | blocks | ' +
|
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
||||||
'removeformat | table | code | help',
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 368px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
placeholder: 'Start typing...',
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
// Critical settings to prevent cursor jumping
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
keep_styles: true,
|
|
||||||
// Disable all problematic features
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
// Content handling
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
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',
|
|
||||||
// Prevent automatic updates
|
|
||||||
element_format: 'html',
|
|
||||||
valid_children: '+body[style]',
|
|
||||||
extended_valid_elements: 'span[*]',
|
|
||||||
custom_elements: '~span',
|
|
||||||
// Mobile support
|
|
||||||
mobile: {
|
|
||||||
theme: 'silver',
|
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StaticEditor;
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// components/strict-readonly-editor.js
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
function StrictReadOnlyEditor(props) {
|
|
||||||
const editorRef = useRef(null);
|
|
||||||
|
|
||||||
const handleInit = (evt, editor) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
// Disable all possible editing events
|
|
||||||
const disableEvents = ['keydown', 'keyup', 'keypress', 'input', 'paste', 'drop', 'cut', 'copy'];
|
|
||||||
|
|
||||||
disableEvents.forEach(eventType => {
|
|
||||||
editor.on(eventType, (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable mouse events that might allow editing
|
|
||||||
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
|
||||||
if (e.target.closest('.mce-content-body')) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable focus events
|
|
||||||
editor.on('focus blur', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
onInit={handleInit}
|
|
||||||
initialValue={props.initialData || ''}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={{
|
|
||||||
height: props.height || 400,
|
|
||||||
menubar: false,
|
|
||||||
toolbar: false,
|
|
||||||
plugins: [
|
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
|
||||||
'insertdatetime', 'media', 'table'
|
|
||||||
],
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
padding: 16px;
|
|
||||||
min-height: ${(props.height || 400) - 32}px;
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
.mce-content-body * {
|
|
||||||
pointer-events: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
readonly: true,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: false,
|
|
||||||
auto_focus: false,
|
|
||||||
forced_root_block: 'p',
|
|
||||||
entity_encoding: 'raw',
|
|
||||||
verify_html: false,
|
|
||||||
cleanup: false,
|
|
||||||
cleanup_on_startup: false,
|
|
||||||
auto_resize: false,
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
paste_as_text: true,
|
|
||||||
paste_enable_default_filters: false,
|
|
||||||
contextmenu: false,
|
|
||||||
selection: false,
|
|
||||||
object_resizing: false,
|
|
||||||
element_format: 'html',
|
|
||||||
// Additional strict settings
|
|
||||||
valid_children: false,
|
|
||||||
extended_valid_elements: false,
|
|
||||||
custom_elements: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StrictReadOnlyEditor;
|
|
||||||
|
|
@ -1,303 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useRef, useState, useEffect } from "react";
|
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
|
||||||
|
|
||||||
interface TinyMCEEditorProps {
|
|
||||||
initialData?: string;
|
|
||||||
onChange?: (data: string) => void;
|
|
||||||
onReady?: (editor: any) => void;
|
|
||||||
height?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
features?: "basic" | "standard" | "full";
|
|
||||||
toolbar?: string;
|
|
||||||
language?: string;
|
|
||||||
uploadUrl?: string;
|
|
||||||
uploadHeaders?: Record<string, string>;
|
|
||||||
className?: string;
|
|
||||||
autoSave?: boolean;
|
|
||||||
autoSaveInterval?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|
||||||
initialData = "",
|
|
||||||
onChange,
|
|
||||||
onReady,
|
|
||||||
height = 400,
|
|
||||||
placeholder = "Start typing...",
|
|
||||||
disabled = false,
|
|
||||||
readOnly = false,
|
|
||||||
features = "standard",
|
|
||||||
toolbar,
|
|
||||||
language = "en",
|
|
||||||
uploadUrl,
|
|
||||||
uploadHeaders,
|
|
||||||
className = "",
|
|
||||||
autoSave = true,
|
|
||||||
autoSaveInterval = 30000,
|
|
||||||
}) => {
|
|
||||||
const editorRef = useRef<any>(null);
|
|
||||||
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
|
||||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
||||||
const [wordCount, setWordCount] = useState(0);
|
|
||||||
|
|
||||||
// Feature-based configurations
|
|
||||||
const getFeatureConfig = (featureLevel: string) => {
|
|
||||||
const configs = {
|
|
||||||
basic: {
|
|
||||||
plugins: ["lists", "link", "autolink", "wordcount"],
|
|
||||||
toolbar: "bold italic | bullist numlist | link",
|
|
||||||
menubar: false,
|
|
||||||
},
|
|
||||||
standard: {
|
|
||||||
plugins: [
|
|
||||||
"advlist",
|
|
||||||
"autolink",
|
|
||||||
"lists",
|
|
||||||
"link",
|
|
||||||
"image",
|
|
||||||
"charmap",
|
|
||||||
"preview",
|
|
||||||
"anchor",
|
|
||||||
"searchreplace",
|
|
||||||
"visualblocks",
|
|
||||||
"code",
|
|
||||||
"fullscreen",
|
|
||||||
"insertdatetime",
|
|
||||||
"media",
|
|
||||||
"table",
|
|
||||||
"help",
|
|
||||||
"wordcount",
|
|
||||||
],
|
|
||||||
toolbar:
|
|
||||||
"undo redo | blocks | " +
|
|
||||||
"bold italic forecolor | alignleft aligncenter " +
|
|
||||||
"alignright alignjustify | bullist numlist outdent indent | " +
|
|
||||||
"removeformat | table | code | help",
|
|
||||||
menubar: false,
|
|
||||||
},
|
|
||||||
full: {
|
|
||||||
plugins: [
|
|
||||||
"advlist",
|
|
||||||
"autolink",
|
|
||||||
"lists",
|
|
||||||
"link",
|
|
||||||
"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 | " +
|
|
||||||
"alignleft aligncenter alignright alignjustify | " +
|
|
||||||
"bullist numlist outdent indent | removeformat | help",
|
|
||||||
menubar: "file edit view insert format tools table help",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return configs[featureLevel as keyof typeof configs] || configs.standard;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditorChange = (content: string) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditorInit = (evt: any, editor: any) => {
|
|
||||||
editorRef.current = editor;
|
|
||||||
setIsEditorLoaded(true);
|
|
||||||
|
|
||||||
if (onReady) {
|
|
||||||
onReady(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up word count tracking
|
|
||||||
editor.on("keyup", () => {
|
|
||||||
const count = editor.plugins.wordcount.body.getCharacterCount();
|
|
||||||
setWordCount(count);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up auto-save
|
|
||||||
if (autoSave && !readOnly) {
|
|
||||||
setInterval(() => {
|
|
||||||
const content = editor.getContent();
|
|
||||||
localStorage.setItem("tinymce-autosave", content);
|
|
||||||
setLastSaved(new Date());
|
|
||||||
}, autoSaveInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix cursor jumping issues
|
|
||||||
editor.on("keyup", (e: any) => {
|
|
||||||
// Prevent cursor jumping on content changes
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.on("input", (e: any) => {
|
|
||||||
// Prevent unnecessary re-renders
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle paste events properly
|
|
||||||
editor.on("paste", (e: any) => {
|
|
||||||
// Allow default paste behavior
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageUpload = (blobInfo: any, progress: any) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!uploadUrl) {
|
|
||||||
reject("No upload URL configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", blobInfo.blob(), blobInfo.filename());
|
|
||||||
|
|
||||||
fetch(uploadUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: uploadHeaders || {},
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((result) => {
|
|
||||||
resolve(result.url);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const featureConfig = getFeatureConfig(features);
|
|
||||||
|
|
||||||
const editorConfig = {
|
|
||||||
height,
|
|
||||||
language,
|
|
||||||
placeholder,
|
|
||||||
branding: false,
|
|
||||||
elementpath: false,
|
|
||||||
resize: false,
|
|
||||||
statusbar: !readOnly,
|
|
||||||
// Performance optimizations
|
|
||||||
cache_suffix: "?v=1.0",
|
|
||||||
browser_spellcheck: false,
|
|
||||||
gecko_spellcheck: false,
|
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.mce-content-body {
|
|
||||||
min-height: ${height - 32}px;
|
|
||||||
}
|
|
||||||
.mce-content-body:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
|
|
||||||
automatic_uploads: !!uploadUrl,
|
|
||||||
file_picker_types: "image",
|
|
||||||
mobile: {
|
|
||||||
theme: "silver",
|
|
||||||
plugins: ["lists", "autolink", "link", "image", "table"],
|
|
||||||
toolbar: "bold italic | bullist numlist | link image",
|
|
||||||
},
|
|
||||||
paste_as_text: false,
|
|
||||||
paste_enable_default_filters: true,
|
|
||||||
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",
|
|
||||||
table_default_styles: { width: "100%" },
|
|
||||||
table_default_attributes: { border: "1" },
|
|
||||||
codesample_languages: [
|
|
||||||
{ text: "HTML/XML", value: "markup" },
|
|
||||||
{ text: "JavaScript", value: "javascript" },
|
|
||||||
{ text: "CSS", value: "css" },
|
|
||||||
{ text: "PHP", value: "php" },
|
|
||||||
{ text: "Python", value: "python" },
|
|
||||||
{ text: "Java", value: "java" },
|
|
||||||
{ text: "C", value: "c" },
|
|
||||||
{ text: "C++", value: "cpp" },
|
|
||||||
],
|
|
||||||
...featureConfig,
|
|
||||||
...(toolbar && { toolbar }),
|
|
||||||
setup: (editor: any) => {
|
|
||||||
// ⬅️ Set readOnly di sini
|
|
||||||
editor.on("init", () => {
|
|
||||||
if (readOnly) {
|
|
||||||
editor.mode.set("readonly");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`tinymce-editor-container ${className}`}>
|
|
||||||
<Editor
|
|
||||||
onInit={handleEditorInit}
|
|
||||||
initialValue={initialData}
|
|
||||||
onEditorChange={handleEditorChange}
|
|
||||||
disabled={disabled}
|
|
||||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
|
||||||
init={editorConfig}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Status bar */}
|
|
||||||
{isEditorLoaded && (
|
|
||||||
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span>
|
|
||||||
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
|
|
||||||
</span>
|
|
||||||
{lastSaved && autoSave && !readOnly && (
|
|
||||||
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
|
||||||
)}
|
|
||||||
<span>• {wordCount} characters</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
||||||
{features} mode
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Performance indicator */}
|
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
|
||||||
Bundle size:{" "}
|
|
||||||
{features === "basic"
|
|
||||||
? "~150KB"
|
|
||||||
: features === "standard"
|
|
||||||
? "~200KB"
|
|
||||||
: "~300KB"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TinyMCEEditor;
|
|
||||||
|
|
@ -3,262 +3,17 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function ViewEditor(props) {
|
function ViewEditor(props) {
|
||||||
const maxHeight = props.maxHeight || 600; // Default max height 600px
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ckeditor-view-wrapper">
|
|
||||||
<CKEditor
|
<CKEditor
|
||||||
editor={Editor}
|
editor={Editor}
|
||||||
data={props.initialData}
|
data={props.initialData}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
config={{
|
config={{
|
||||||
|
// toolbar: [],
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
content_style: `
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #111;
|
|
||||||
background: #fff;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 1em 0 0.5em 0;
|
|
||||||
}
|
|
||||||
ul, ol {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
border-left: 4px solid #d1d5db;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
height: props.height || 400,
|
|
||||||
removePlugins: ["Title"],
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<style jsx>{`
|
|
||||||
.ckeditor-view-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-view-wrapper :global(.ck.ck-editor__main) {
|
|
||||||
min-height: ${props.height || 400}px;
|
|
||||||
max-height: ${maxHeight}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-view-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-color: #fdfdfd;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🌙 Dark mode support */
|
|
||||||
:global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
|
||||||
background-color: #111 !important;
|
|
||||||
color: #f9fafb !important;
|
|
||||||
border-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h1,
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h2,
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h3,
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h4,
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h5,
|
|
||||||
:global(.dark) .ckeditor-view-wrapper h6 {
|
|
||||||
color: #f9fafb !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .ckeditor-view-wrapper blockquote {
|
|
||||||
background-color: #1f2937 !important;
|
|
||||||
border-left: 4px solid #374151 !important;
|
|
||||||
color: #f3f4f6 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar styling */
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
|
||||||
background: #cbd5e1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🌙 Dark mode scrollbar */
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
|
||||||
background: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
|
||||||
background: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-view-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read-only specific styling */
|
|
||||||
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide toolbar */
|
|
||||||
.ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewEditor;
|
export default ViewEditor;
|
||||||
|
|
||||||
// import React from "react";
|
|
||||||
// import { CKEditor } from "@ckeditor/ckeditor5-react";
|
|
||||||
// import Editor from "ckeditor5-custom-build";
|
|
||||||
|
|
||||||
// function ViewEditor(props) {
|
|
||||||
// const maxHeight = props.maxHeight || 600;
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="ckeditor-view-wrapper">
|
|
||||||
// <CKEditor
|
|
||||||
// editor={Editor}
|
|
||||||
// data={props.initialData}
|
|
||||||
// disabled={true}
|
|
||||||
// config={{
|
|
||||||
// // toolbar: [],
|
|
||||||
// isReadOnly: true,
|
|
||||||
// // Add content styling configuration for read-only mode
|
|
||||||
// content_style: `
|
|
||||||
// body {
|
|
||||||
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
// font-size: 14px;
|
|
||||||
// line-height: 1.6;
|
|
||||||
// color: #333;
|
|
||||||
// margin: 0;
|
|
||||||
// padding: 0;
|
|
||||||
// }
|
|
||||||
// p {
|
|
||||||
// margin: 0.5em 0;
|
|
||||||
// }
|
|
||||||
// h1, h2, h3, h4, h5, h6 {
|
|
||||||
// margin: 1em 0 0.5em 0;
|
|
||||||
// }
|
|
||||||
// ul, ol {
|
|
||||||
// margin: 0.5em 0;
|
|
||||||
// padding-left: 2em;
|
|
||||||
// }
|
|
||||||
// blockquote {
|
|
||||||
// margin: 1em 0;
|
|
||||||
// padding: 0.5em 1em;
|
|
||||||
// border-left: 4px solid #d1d5db;
|
|
||||||
// background-color: #f9fafb;
|
|
||||||
// }
|
|
||||||
// `,
|
|
||||||
// // Editor appearance settings
|
|
||||||
// height: props.height || 400,
|
|
||||||
// removePlugins: ['Title'],
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <style jsx>{`
|
|
||||||
// .ckeditor-view-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-view-wrapper :global(.ck.ck-editor__main) {
|
|
||||||
// min-height: ${props.height || 400}px;
|
|
||||||
// max-height: ${maxHeight}px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .ckeditor-view-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-color:rgb(253, 253, 253);
|
|
||||||
// border: 1px solid #d1d5db;
|
|
||||||
// border-radius: 6px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /* Custom scrollbar styling for webkit browsers */
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
|
||||||
// width: 8px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
|
||||||
// background: #f1f5f9;
|
|
||||||
// border-radius: 4px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
|
||||||
// background: #cbd5e1;
|
|
||||||
// border-radius: 4px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
|
||||||
// background: #94a3b8;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /* Ensure content doesn't overflow */
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
|
||||||
// overflow: hidden;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /* Read-only specific styling */
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
|
||||||
// background-color: #f8fafc;
|
|
||||||
// color: #4b5563;
|
|
||||||
// cursor: default;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /* Hide toolbar for view-only mode */
|
|
||||||
// .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
|
||||||
// display: none !important;
|
|
||||||
// }
|
|
||||||
// `}</style>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default ViewEditor;
|
|
||||||
|
|
|
||||||
|
|
@ -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 { convertDateFormatNoTime, htmlToString } from "@/utils/global";
|
import { 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,13 +44,6 @@ 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(
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -89,9 +82,6 @@ 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",
|
||||||
}),
|
}),
|
||||||
|
|
@ -104,7 +94,6 @@ 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() {
|
||||||
|
|
@ -128,8 +117,8 @@ export default function CreateArticleForm() {
|
||||||
"publish"
|
"publish"
|
||||||
);
|
);
|
||||||
const [isScheduled, setIsScheduled] = useState(false);
|
const [isScheduled, setIsScheduled] = useState(false);
|
||||||
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
|
|
||||||
const [startTimeValue, setStartTimeValue] = useState<string>("");
|
const [startDateValue, setStartDateValue] = useState<any>(null);
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
onDrop: (acceptedFiles) => {
|
onDrop: (acceptedFiles) => {
|
||||||
|
|
@ -236,8 +225,6 @@ 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,
|
||||||
|
|
@ -293,8 +280,6 @@ 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)),
|
||||||
|
|
@ -339,34 +324,12 @@ export default function CreateArticleForm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "scheduled" && startDateValue) {
|
if (status === "scheduled") {
|
||||||
// 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: formattedDateTime,
|
date: `${startDateValue?.year}-${startDateValue?.month}-${startDateValue?.day}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📤 Sending schedule request:", request);
|
|
||||||
const res = await createArticleSchedule(request);
|
const res = await createArticleSchedule(request);
|
||||||
console.log("✅ Schedule response:", res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close();
|
close();
|
||||||
|
|
@ -561,21 +524,13 @@ 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 : ""
|
||||||
|
|
@ -696,38 +651,7 @@ 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}
|
||||||
|
|
@ -740,10 +664,7 @@ export default function CreateArticleForm() {
|
||||||
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
|
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
|
||||||
}}
|
}}
|
||||||
classNamePrefix="select"
|
classNamePrefix="select"
|
||||||
value={value}
|
onChange={onChange}
|
||||||
onChange={(selected) => {
|
|
||||||
onChange(selected);
|
|
||||||
}}
|
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
components={animatedComponents}
|
components={animatedComponents}
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
|
|
@ -762,7 +683,60 @@ export default function CreateArticleForm() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm">Tags</p>
|
<p className="text-sm">Tags</p>
|
||||||
|
{/* <Controller
|
||||||
|
control={control}
|
||||||
|
name="tags"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Textarea
|
||||||
|
type="text"
|
||||||
|
id="tags"
|
||||||
|
placeholder=""
|
||||||
|
label=""
|
||||||
|
value={tag}
|
||||||
|
onValueChange={setTag}
|
||||||
|
startContent={
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{value.map((item, index) => (
|
||||||
|
<Chip
|
||||||
|
color="primary"
|
||||||
|
key={index}
|
||||||
|
className=""
|
||||||
|
onClose={() => {
|
||||||
|
const filteredTags = value.filter((tag) => tag !== item);
|
||||||
|
if (filteredTags.length === 0) {
|
||||||
|
setError("tags", {
|
||||||
|
type: "manual",
|
||||||
|
message: "Tags tidak boleh kosong",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clearErrors("tags");
|
||||||
|
setValue("tags", filteredTags as [string, ...string[]]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (tag.trim() !== "") {
|
||||||
|
setValue("tags", [...value, tag.trim()]);
|
||||||
|
setTag("");
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
labelPlacement="outside"
|
||||||
|
className="w-full h-fit"
|
||||||
|
classNames={{
|
||||||
|
inputWrapper: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
|
||||||
|
}}
|
||||||
|
variant="bordered"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="tags"
|
name="tags"
|
||||||
|
|
@ -841,49 +815,32 @@ export default function CreateArticleForm() {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isScheduled && (
|
{/* {isScheduled && (
|
||||||
<div className="flex flex-col lg:flex-row gap-3 mt-2">
|
<div className="flex flex-col lg:flex-row gap-3">
|
||||||
{/* Pilih tanggal */}
|
<div className="w-full lg:w-[140px] flex flex-col gal-2 ">
|
||||||
<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-[37px] lg:h-[37px] border-1 rounded-lg text-black"
|
className="w-full !h-[30px] lg:h-[40px] border-1 rounded-lg text-black"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{startDateValue
|
{startDateValue
|
||||||
? startDateValue.toLocaleDateString("en-CA")
|
? convertDateFormatNoTime(startDateValue)
|
||||||
: "-"}
|
: "-"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="bg-transparent p-0">
|
<PopoverContent className="bg-transparent p-0">
|
||||||
<DatePicker
|
<Calendar
|
||||||
selected={startDateValue}
|
selected={startDateValue}
|
||||||
onChange={(date) =>
|
onSelect={setStartDateValue}
|
||||||
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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,10 @@ 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,
|
||||||
|
|
@ -51,15 +48,6 @@ 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(
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -93,9 +81,6 @@ 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",
|
||||||
}),
|
}),
|
||||||
|
|
@ -107,8 +92,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",
|
||||||
}),
|
}), // Array berisi string
|
||||||
source: z.enum(["internal", "external"]).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface DiseData {
|
interface DiseData {
|
||||||
|
|
@ -152,14 +136,8 @@ 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) => {
|
||||||
|
|
@ -197,50 +175,20 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
async function initState() {
|
async function initState() {
|
||||||
loading();
|
loading();
|
||||||
try {
|
const res = await getArticleById(id);
|
||||||
// 1️⃣ Ambil ARTICLE
|
const data = res.data?.data;
|
||||||
const articleRes = await getArticleById(id);
|
setDetailData(data);
|
||||||
const articleData = articleRes.data?.data;
|
setValue("title", data?.title);
|
||||||
|
setValue("slug", data?.slug);
|
||||||
|
setValue("description", data?.htmlDescription);
|
||||||
|
setValue("tags", data?.tags ? data.tags.split(",") : []);
|
||||||
|
setThumbnail(data?.thumbnailUrl);
|
||||||
|
setDiseId(data?.aiArticleId);
|
||||||
|
setDetailFiles(data?.files);
|
||||||
|
|
||||||
if (!articleData) return;
|
setupInitCategory(data?.categories);
|
||||||
|
|
||||||
// ===== 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();
|
close();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const setupInitCategory = (data: any) => {
|
const setupInitCategory = (data: any) => {
|
||||||
const temp: CategoryType[] = [];
|
const temp: CategoryType[] = [];
|
||||||
|
|
@ -294,28 +242,21 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
const doPublish = async () => {
|
const doPublish = async () => {
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
title: isScheduled ? "Jadwalkan Publikasi?" : "Publish Artikel Sekarang?",
|
title: "Publish Artikel?",
|
||||||
text: isScheduled
|
text: "",
|
||||||
? "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: isScheduled ? "Jadwalkan" : "Publish",
|
confirmButtonText: "Submit",
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
if (isScheduled) {
|
publish();
|
||||||
setStatus("scheduled");
|
|
||||||
publishScheduled();
|
|
||||||
} else {
|
|
||||||
publishNow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const publishNow = async () => {
|
const publish = async () => {
|
||||||
const response = await updateArticle(String(id), {
|
const response = await updateArticle(String(id), {
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
isPublish: true,
|
isPublish: true,
|
||||||
|
|
@ -332,67 +273,8 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
error(response.message);
|
error(response.message);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -410,16 +292,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) {
|
||||||
|
|
@ -436,38 +318,8 @@ 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();
|
||||||
successSubmitData();
|
successSubmit("/admin/article");
|
||||||
};
|
};
|
||||||
|
|
||||||
function successSubmit(redirect: string) {
|
function successSubmit(redirect: string) {
|
||||||
|
|
@ -483,51 +335,6 @@ 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
|
||||||
|
|
@ -690,10 +497,9 @@ 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-xl font-medium mb-2">
|
<label htmlFor="title" className="block text-sm font-medium mb-1">
|
||||||
Judul
|
Judul
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="title"
|
id="title"
|
||||||
|
|
@ -701,7 +507,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
readOnly={isDetail}
|
readOnly={isDetail}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="h-16 px-4 text-2xl leading-tight"
|
className="w-full border rounded-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -743,7 +549,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">Deskripsi</p>
|
<p className="text-sm mt-3">Deskripsi</p>
|
||||||
{/* <Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field: { onChange, value } }) =>
|
render={({ field: { onChange, value } }) =>
|
||||||
|
|
@ -766,17 +572,8 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
<p className="text-red-400 text-sm mb-3">
|
<p className="text-red-400 text-sm mb-3">
|
||||||
{errors.description?.message}
|
{errors.description?.message}
|
||||||
</p>
|
</p>
|
||||||
)} */}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomEditor onChange={field.onChange} initialData={field.value} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.description?.message && (
|
|
||||||
<p className="text-red-400 text-sm">{errors.description.message}</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">File Media</p>
|
<p className="text-sm mt-3">File Media</p>
|
||||||
{!isDetail && (
|
{!isDetail && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
@ -817,7 +614,9 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
alt="main"
|
alt="main"
|
||||||
width={720}
|
width={720}
|
||||||
height={480}
|
height={480}
|
||||||
src={detailfiles[mainImage]?.fileUrl || "/default-avatar.png"}
|
src={
|
||||||
|
detailfiles[mainImage]?.file_url || "/default-avatar.png"
|
||||||
|
}
|
||||||
className="w-[75%] mx-auto"
|
className="w-[75%] mx-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -832,7 +631,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
width={480}
|
width={480}
|
||||||
height={360}
|
height={360}
|
||||||
alt={`image-${index}`}
|
alt={`image-${index}`}
|
||||||
src={file.fileUrl || "/default-avatar.png"}
|
src={file.file_url || "/default-avatar.png"}
|
||||||
className="h-[100px] object-cover w-[150px]"
|
className="h-[100px] object-cover w-[150px]"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -855,7 +654,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
width={480}
|
width={480}
|
||||||
height={360}
|
height={360}
|
||||||
alt={`image-${index}`}
|
alt={`image-${index}`}
|
||||||
src={file?.fileUrl || "/default-avatar.png"}
|
src={file?.file_url || "/default-avatar.png"}
|
||||||
className="h-[100px] object-cover w-[150px]"
|
className="h-[100px] object-cover w-[150px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -875,7 +674,6 @@ 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"
|
||||||
|
|
@ -977,43 +775,6 @@ 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}
|
||||||
|
|
@ -1115,63 +876,7 @@ 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>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 mt-3">
|
{!isDetail && username === "admin-mabes" && (
|
||||||
<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">
|
||||||
|
|
@ -1203,7 +908,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">
|
||||||
|
|
@ -1240,14 +945,12 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{detailData?.isPublish === false && (
|
{isDetail &&
|
||||||
<Button
|
detailData?.isPublish === false &&
|
||||||
type="button"
|
detailData?.statusId !== 1 &&
|
||||||
color="primary"
|
Number(userId) === detailData?.createdById && (
|
||||||
onClick={doPublish}
|
<Button type="button" color="primary" onClick={doPublish}>
|
||||||
disabled={isScheduled && !startDateValue}
|
Publish
|
||||||
>
|
|
||||||
{isScheduled ? "Jadwalkan" : "Publish"}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* {!isDetail && (
|
{/* {!isDetail && (
|
||||||
|
|
@ -1256,17 +959,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||||
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 {
|
import { getDetailArticle, getGenerateRewriter } from "@/service/generate-article";
|
||||||
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";
|
||||||
|
|
@ -78,11 +69,8 @@ interface DiseData {
|
||||||
additionalKeywords: string;
|
additionalKeywords: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenerateContentRewriteForm(props: {
|
export default function GenerateContentRewriteForm(props: { content: (data: DiseData) => void }) {
|
||||||
content: (data: DiseData) => void;
|
const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational");
|
||||||
}) {
|
|
||||||
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("");
|
||||||
|
|
@ -178,10 +166,7 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
))}
|
))}
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select
|
<Select value={selectedWritingSyle} onValueChange={(value) => setSelectedWritingStyle(value)}>
|
||||||
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>
|
||||||
|
|
@ -213,10 +198,7 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
))}
|
))}
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select
|
<Select value={selectedArticleSize} onValueChange={(value) => setSelectedArticleSize(value)}>
|
||||||
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>
|
||||||
|
|
@ -247,10 +229,7 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
<SelectItem key="en">English</SelectItem>
|
<SelectItem key="en">English</SelectItem>
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select
|
<Select value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)}>
|
||||||
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>
|
||||||
|
|
@ -260,7 +239,6 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
</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>
|
||||||
|
|
@ -268,16 +246,9 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
<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 == "" && (
|
{mainKeyword == "" && <p className="text-red-400 text-sm">Required</p>}
|
||||||
<p className="text-red-400 text-sm">Required</p>
|
|
||||||
)}
|
|
||||||
{articleIds.length < 3 && (
|
{articleIds.length < 3 && (
|
||||||
<Button
|
<Button onClick={onSubmit} type="button" disabled={mainKeyword === "" || isLoading} className="my-5 w-full py-5 text-xs md:text-base">
|
||||||
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" />
|
||||||
|
|
@ -292,14 +263,7 @@ export default function GenerateContentRewriteForm(props: {
|
||||||
{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
|
<Button key={id} onClick={() => setSelectedId(id)} disabled={isLoading && selectedId === id} variant={selectedId === id ? "default" : "outline"} className="flex items-center gap-2">
|
||||||
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" />
|
||||||
|
|
|
||||||
|
|
@ -76,15 +76,16 @@ interface DiseData {
|
||||||
export default function GenerateSingleArticleForm(props: {
|
export default function GenerateSingleArticleForm(props: {
|
||||||
content: (data: DiseData) => void;
|
content: (data: DiseData) => void;
|
||||||
}) {
|
}) {
|
||||||
const [selectedWritingSyle, setSelectedWritingStyle] = useState("");
|
const [selectedWritingSyle, setSelectedWritingStyle] =
|
||||||
const [selectedArticleSize, setSelectedArticleSize] = useState("");
|
useState("Informational");
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState("");
|
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
|
||||||
|
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(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const generateAll = async (keyword: string | undefined) => {
|
const generateAll = async (keyword: string | undefined) => {
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
|
|
@ -270,11 +271,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="Article Size" />
|
<SelectValue placeholder="Writing Style" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{articleSize.map((style) => (
|
{articleSize.map((style) => (
|
||||||
<SelectItem key={style.name} value={style.value}>
|
<SelectItem key={style.name} value={style.name}>
|
||||||
{style.name}
|
{style.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -306,7 +307,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="Language" />
|
<SelectValue placeholder="Bahasa" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="id">Indonesia</SelectItem>
|
<SelectItem value="id">Indonesia</SelectItem>
|
||||||
|
|
@ -318,7 +319,6 @@ 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,7 +350,6 @@ 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)}
|
||||||
|
|
@ -374,7 +373,6 @@ 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)}
|
||||||
|
|
@ -419,7 +417,6 @@ 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}
|
||||||
|
|
|
||||||
|
|
@ -1,451 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { getListArticle } from "@/service/article";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import news from "../news";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { getAdvertise } from "@/service/advertisement";
|
|
||||||
|
|
||||||
type Article = {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
categoryName: string;
|
|
||||||
createdAt: string;
|
|
||||||
slug: string;
|
|
||||||
createdByName: string;
|
|
||||||
customCreatorName: string;
|
|
||||||
thumbnailUrl: string;
|
|
||||||
categories: {
|
|
||||||
title: string;
|
|
||||||
}[];
|
|
||||||
files: {
|
|
||||||
fileUrl: string;
|
|
||||||
file_alt: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const slugToLabel = (slug: string) => {
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
development: "Pembangunan",
|
|
||||||
health: "Kesehatan",
|
|
||||||
"citizen-news": "Berita Warga",
|
|
||||||
};
|
|
||||||
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() {
|
|
||||||
const [activeTab, setActiveTab] = useState("comments");
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPage, setTotalPage] = useState(1);
|
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
|
||||||
const [showData, setShowData] = useState("100");
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pathname = usePathname();
|
|
||||||
const pathSegments = pathname.split("/").filter(Boolean);
|
|
||||||
|
|
||||||
const categorySlug = pathSegments[1];
|
|
||||||
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(() => {
|
|
||||||
initState();
|
|
||||||
}, [page, showData, startDateValue, selectedCategories, activeTab]);
|
|
||||||
|
|
||||||
async function initState() {
|
|
||||||
let sortBy = "created_at";
|
|
||||||
if (activeTab === "comments") sortBy = "comment_count";
|
|
||||||
if (activeTab === "trending") sortBy = "view_count";
|
|
||||||
|
|
||||||
// loading();
|
|
||||||
const req = {
|
|
||||||
limit: showData,
|
|
||||||
page: 1,
|
|
||||||
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);
|
|
||||||
} finally {
|
|
||||||
// close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const citizenArticles = articles.filter((article) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pagination manually (front-end)
|
|
||||||
const itemsPerPage = 2;
|
|
||||||
const calculatedTotalPage = Math.ceil(citizenArticles.length / itemsPerPage);
|
|
||||||
|
|
||||||
const paginatedArticles = citizenArticles.slice(
|
|
||||||
(page - 1) * itemsPerPage,
|
|
||||||
page * itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
function truncateText(text: string, wordLimit: number) {
|
|
||||||
const words = text.split(" ");
|
|
||||||
if (words.length <= wordLimit) return text;
|
|
||||||
return words.slice(0, wordLimit).join(" ") + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white grid grid-cols-1 lg:grid-cols-3 gap-6 py-10 px-8">
|
|
||||||
{/* Left Content */}
|
|
||||||
<div className="lg:col-span-2 space-y-10">
|
|
||||||
{paginatedArticles.map((item) => (
|
|
||||||
<div key={item.id}>
|
|
||||||
<Link
|
|
||||||
className="flex flex-col md:flex-row gap-6"
|
|
||||||
href={`/details/${item?.slug}`}
|
|
||||||
>
|
|
||||||
{/* Image + Category */}
|
|
||||||
<div className="relative w-full md:w-1/2 h-64">
|
|
||||||
<Image
|
|
||||||
src={item.thumbnailUrl || "/placeholder.png"}
|
|
||||||
alt={item.title}
|
|
||||||
fill
|
|
||||||
className="object-cover rounded"
|
|
||||||
/>
|
|
||||||
<span className="absolute top-3 left-3 bg-yellow-400 text-black px-3 py-1 text-xs font-bold">
|
|
||||||
{item.categories[0]?.title || "Pembangunan"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-xl font-bold text-[#16324F] hover:text-green-600 cursor-pointer">
|
|
||||||
{item.title}
|
|
||||||
</h2>
|
|
||||||
<div className="text-sm text-gray-600 mt-2">
|
|
||||||
BY{" "}
|
|
||||||
<span className="text-green-600 font-semibold">
|
|
||||||
{item?.customCreatorName || item.createdByName}
|
|
||||||
</span>{" "}
|
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
|
||||||
</div>
|
|
||||||
<p className="mt-3 text-gray-700">
|
|
||||||
{truncateText(item.description, 20)}
|
|
||||||
</p>
|
|
||||||
<button className="mt-4 px-4 py-2 border border-gray-400 text-gray-700 hover:bg-black hover:text-white transition">
|
|
||||||
READ MORE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
<div className="flex items-center justify-center gap-2 mt-6">
|
|
||||||
{/* Previous Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
|
|
||||||
disabled={page === 1}
|
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Page Numbers */}
|
|
||||||
{Array.from({ length: calculatedTotalPage }, (_, i) => i + 1)
|
|
||||||
.filter((p) => {
|
|
||||||
// Always show first, last, current, and pages around current
|
|
||||||
return (
|
|
||||||
p === 1 ||
|
|
||||||
p === calculatedTotalPage ||
|
|
||||||
(p >= page - 1 && p <= page + 1)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((p, idx, arr) => {
|
|
||||||
const prev = arr[idx - 1];
|
|
||||||
const showEllipsis = prev && p - prev > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={p} className="flex items-center">
|
|
||||||
{showEllipsis && <span className="px-2">...</span>}
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(p)}
|
|
||||||
className={`px-3 py-1 ${
|
|
||||||
page === p ? "bg-green-600 text-white" : "border"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{p}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Next Button */}
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setPage((prev) => Math.min(prev + 1, calculatedTotalPage))
|
|
||||||
}
|
|
||||||
disabled={page === calculatedTotalPage}
|
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Advertisement */}
|
|
||||||
<div className="w-full h-[400px] relative">
|
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Connect with us */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Connect with us</h3>
|
|
||||||
<div className="h-1 w-20 bg-green-600 mb-4"></div>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="bg-blue-500 text-white text-center p-3">
|
|
||||||
<p className="text-xl font-bold">138</p>
|
|
||||||
<p className="text-sm">Followers</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-600 text-white text-center p-3">
|
|
||||||
<p className="text-xl font-bold">205k</p>
|
|
||||||
<p className="text-sm">Subscribers</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-yellow-400 text-black text-center p-3">
|
|
||||||
<p className="text-xl font-bold">23.9k</p>
|
|
||||||
<p className="text-sm">Followers</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div>
|
|
||||||
<div className="flex gap-4 border-b">
|
|
||||||
{["trending", "comments", "latest"].map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
className={`pb-2 capitalize ${
|
|
||||||
activeTab === tab
|
|
||||||
? "border-b-2 border-green-600 text-green-600"
|
|
||||||
: "text-gray-600"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setPage(1); // reset page setiap ganti tab
|
|
||||||
setActiveTab(tab);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
{articles.slice(0, 5).map((item) => (
|
|
||||||
<div key={item.id} className="flex gap-3 items-center">
|
|
||||||
<Link
|
|
||||||
className="flex gap-3 items-center"
|
|
||||||
href={`/details/${item?.slug}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
|
||||||
alt={item.title}
|
|
||||||
width={80}
|
|
||||||
height={60}
|
|
||||||
className="object-cover rounded"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-sm">{item.title}</h3>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{new Date(item.createdAt).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="">
|
|
||||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
|
||||||
Recommended
|
|
||||||
</h2>
|
|
||||||
<div className=" w-full">
|
|
||||||
<div className="relative w-full aspect-video mb-5">
|
|
||||||
<Link href={`/details/${articles[0]?.slug}`}>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
articles[0]?.thumbnailUrl ||
|
|
||||||
articles[0]?.files?.[0]?.fileUrl ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={"articles[0]?.title"}
|
|
||||||
fill
|
|
||||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/30" />
|
|
||||||
<div className="absolute bottom-0.5 left-2 text-white">
|
|
||||||
<h3 className=" font-semibold text-base mb-1">
|
|
||||||
{articles[0]?.title}
|
|
||||||
</h3>
|
|
||||||
<p className=" text-xs mb-2 flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="none">
|
|
||||||
<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" />
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>{" "}
|
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-5">
|
|
||||||
{articles?.slice(1, 4).map((article, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Link
|
|
||||||
className="flex gap-3"
|
|
||||||
href={`/details/${article?.slug}`}
|
|
||||||
>
|
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
article?.thumbnailUrl ||
|
|
||||||
article?.files?.[0]?.fileUrl ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={"article?.title"}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold mb-3">
|
|
||||||
{article?.title}
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-gray-500 flex gap-2 items-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="none">
|
|
||||||
<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" />
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>{" "}
|
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -12,15 +12,13 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -38,7 +36,7 @@ export default function HeaderCitizen() {
|
||||||
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[]>([]);
|
||||||
const [showData, setShowData] = useState("100");
|
const [showData, setShowData] = useState("5");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
|
@ -77,15 +75,6 @@ export default function HeaderCitizen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const citizenArticles = articles.filter((article) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainArticle = citizenArticles[0];
|
|
||||||
const otherArticles = citizenArticles.slice(1, 3);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-7xl mx-auto bg-white">
|
<section className="max-w-7xl mx-auto bg-white">
|
||||||
<div className="flex flex-col items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
<div className="flex flex-col items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
||||||
|
|
@ -108,28 +97,30 @@ export default function HeaderCitizen() {
|
||||||
|
|
||||||
<div className="pb-5">
|
<div className="pb-5">
|
||||||
<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 && (
|
{articles.length > 0 && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/details/${mainArticle.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
src={
|
||||||
alt={mainArticle.title}
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
"/default-image.jpg"
|
||||||
|
}
|
||||||
|
alt={articles[0].title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
className="w-full h-full max-h-[460px] 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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent p-6 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
||||||
{mainArticle.categories?.[0]?.title || "TANPA KATEGORI"}
|
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
</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">
|
<h2 className="text-sm md:text-xl lg:text-2xl font-bold text-white leading-snug mb-2 w-full md:w-9/12">
|
||||||
{mainArticle.title}
|
{articles[0].title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle?.customCreatorName ||
|
{articles[0].createdByName} -{" "}
|
||||||
mainArticle.createdByName}{" "}
|
{new Date(articles[0].createdAt).toLocaleDateString(
|
||||||
-{" "}
|
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|
@ -144,13 +135,13 @@ export default function HeaderCitizen() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{articles.slice(1, 3).map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/details/${article.slug}`}>
|
<Link href={`/detail/${article?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
@ -160,7 +151,7 @@ export default function HeaderCitizen() {
|
||||||
/>
|
/>
|
||||||
<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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
<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"}
|
{article.categoryName || "TANPA KATEGORI"}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||||
{article.title}
|
{article.title}
|
||||||
|
|
@ -171,6 +162,15 @@ export default function HeaderCitizen() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Berita Utama"
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ 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;
|
||||||
|
|
@ -14,27 +13,16 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: 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",
|
||||||
|
|
@ -49,7 +37,7 @@ export default function DevelopmentNews() {
|
||||||
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[]>([]);
|
||||||
const [showData, setShowData] = useState("100");
|
const [showData, setShowData] = useState("2");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
|
@ -60,36 +48,6 @@ 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);
|
||||||
|
|
||||||
|
|
@ -105,7 +63,7 @@ export default function DevelopmentNews() {
|
||||||
// loading();
|
// loading();
|
||||||
const req = {
|
const req = {
|
||||||
limit: showData,
|
limit: showData,
|
||||||
page: 1,
|
page,
|
||||||
search,
|
search,
|
||||||
categorySlug: Array.from(selectedCategories).join(","),
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
sort: "desc",
|
sort: "desc",
|
||||||
|
|
@ -122,23 +80,6 @@ export default function DevelopmentNews() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pembangunanArticles = articles.filter((article) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pagination manually (front-end)
|
|
||||||
const itemsPerPage = 2;
|
|
||||||
const calculatedTotalPage = Math.ceil(
|
|
||||||
pembangunanArticles.length / itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginatedArticles = pembangunanArticles.slice(
|
|
||||||
(page - 1) * itemsPerPage,
|
|
||||||
page * itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
function truncateText(text: string, wordLimit: number) {
|
function truncateText(text: string, wordLimit: number) {
|
||||||
const words = text.split(" ");
|
const words = text.split(" ");
|
||||||
if (words.length <= wordLimit) return text;
|
if (words.length <= wordLimit) return text;
|
||||||
|
|
@ -146,14 +87,14 @@ export default function DevelopmentNews() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white grid grid-cols-1 lg:grid-cols-3 gap-6 py-10 px-8">
|
<div className="bg-white grid grid-cols-1 lg:grid-cols-3 gap-6 py-10 px-4">
|
||||||
{/* Left Content */}
|
{/* Left Content */}
|
||||||
<div className="lg:col-span-2 space-y-10">
|
<div className="lg:col-span-2 space-y-10">
|
||||||
{paginatedArticles.map((item) => (
|
{articles.map((item) => (
|
||||||
<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={`/details/${item?.slug}`}
|
href={`/detail/${item?.id}`}
|
||||||
>
|
>
|
||||||
{/* 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">
|
||||||
|
|
@ -164,7 +105,7 @@ export default function DevelopmentNews() {
|
||||||
className="object-cover rounded"
|
className="object-cover rounded"
|
||||||
/>
|
/>
|
||||||
<span className="absolute top-3 left-3 bg-yellow-400 text-black px-3 py-1 text-xs font-bold">
|
<span className="absolute top-3 left-3 bg-yellow-400 text-black px-3 py-1 text-xs font-bold">
|
||||||
{item.categories[0]?.title || "Pembangunan"}
|
{item.categories[0]?.title || categoryLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -176,7 +117,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?.customCreatorName || item.createdByName}
|
{item.createdByName || "Admin"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -193,51 +134,17 @@ export default function DevelopmentNews() {
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex items-center justify-center gap-2 mt-6">
|
<div className="flex items-center justify-center gap-2 mt-6">
|
||||||
{/* Previous Button */}
|
{Array.from({ length: totalPage }, (_, i) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
|
key={i}
|
||||||
disabled={page === 1}
|
onClick={() => setPage(i + 1)}
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{Array.from({ length: calculatedTotalPage }, (_, i) => i + 1)
|
|
||||||
.filter((p) => {
|
|
||||||
return (
|
|
||||||
p === 1 ||
|
|
||||||
p === calculatedTotalPage ||
|
|
||||||
(p >= page - 1 && p <= page + 1)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((p, idx, arr) => {
|
|
||||||
const prev = arr[idx - 1];
|
|
||||||
const showEllipsis = prev && p - prev > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={p} className="flex items-center">
|
|
||||||
{showEllipsis && <span className="px-2">...</span>}
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(p)}
|
|
||||||
className={`px-3 py-1 ${
|
className={`px-3 py-1 ${
|
||||||
page === p ? "bg-green-600 text-white" : "border"
|
page === i + 1 ? "bg-green-600 text-white" : "border"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{p}
|
{i + 1}
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setPage((prev) => Math.min(prev + 1, calculatedTotalPage))
|
|
||||||
}
|
|
||||||
disabled={page === calculatedTotalPage}
|
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -245,33 +152,13 @@ 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">
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block w-full"
|
|
||||||
>
|
|
||||||
<div className="relative w-full h-[350px] flex justify-center">
|
|
||||||
<Image
|
<Image
|
||||||
src={bannerAd.contentFileUrl}
|
src="/advertisiment.png"
|
||||||
alt={bannerAd.title || "Iklan Banner"}
|
alt="Advertisement"
|
||||||
width={1200} // ukuran dasar untuk responsive
|
fill
|
||||||
height={350}
|
className="object-cover rounded"
|
||||||
className="object-cover w-full h-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
src="/kolom.png"
|
|
||||||
alt="Berita Utama"
|
|
||||||
width={1200}
|
|
||||||
height={188}
|
|
||||||
className="object-contain w-full h-[188px]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connect with us */}
|
{/* Connect with us */}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -315,11 +202,11 @@ export default function DevelopmentNews() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
{articles.slice(0, 5).map((item) => (
|
{articles.map((item) => (
|
||||||
<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={`/details/${item?.slug}`}
|
href={`/detail/${item?.id}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
src={item.thumbnailUrl || "/no-image.jpg"}
|
||||||
|
|
@ -344,11 +231,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={`/details/${articles[0]?.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.fileUrl ||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -394,13 +281,13 @@ export default function DevelopmentNews() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/details/${article?.slug}`}
|
href={`/detail/${article?.id}`}
|
||||||
>
|
>
|
||||||
<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]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,13 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -38,7 +36,7 @@ export default function HeaderDevelopment() {
|
||||||
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[]>([]);
|
||||||
const [showData, setShowData] = useState("100");
|
const [showData, setShowData] = useState("5");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
|
@ -46,10 +44,10 @@ export default function HeaderDevelopment() {
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname(); // e.g., "/category/development"
|
||||||
const pathSegments = pathname.split("/").filter(Boolean);
|
const pathSegments = pathname.split("/").filter(Boolean); // ["category", "development"]
|
||||||
|
|
||||||
const categorySlug = pathSegments[1];
|
const categorySlug = pathSegments[1]; // "development"
|
||||||
const categoryLabel = slugToLabel(categorySlug);
|
const categoryLabel = slugToLabel(categorySlug);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -72,22 +70,11 @@ 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) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainArticle = pembangunanArticles[0];
|
|
||||||
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">
|
||||||
<div className="flex flex-col jus items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
<div className="flex flex-col jus items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
||||||
|
|
@ -114,28 +101,30 @@ export default function HeaderDevelopment() {
|
||||||
|
|
||||||
<div className="pb-5">
|
<div className="pb-5">
|
||||||
<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 && (
|
{articles.length > 0 && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/details/${mainArticle.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
src={
|
||||||
alt={mainArticle.title}
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
"/default-image.jpg"
|
||||||
|
}
|
||||||
|
alt={articles[0].title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
className="w-full h-full max-h-[460px] 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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent p-6 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
||||||
{mainArticle.categories?.[0]?.title || "TANPA KATEGORI"}
|
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
</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">
|
<h2 className="text-sm md:text-xl lg:text-2xl font-bold text-white leading-snug mb-2 w-full md:w-9/12">
|
||||||
{mainArticle.title}
|
{articles[0].title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle?.customCreatorName ||
|
{articles[0].createdByName} -{" "}
|
||||||
mainArticle.createdByName}{" "}
|
{new Date(articles[0].createdAt).toLocaleDateString(
|
||||||
-{" "}
|
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|
@ -150,13 +139,13 @@ export default function HeaderDevelopment() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{articles.slice(1, 3).map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/details/${article.slug}`}>
|
<Link href={`/detail/${article?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
@ -166,7 +155,7 @@ export default function HeaderDevelopment() {
|
||||||
/>
|
/>
|
||||||
<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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
<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"}
|
{article.categoryName || "TANPA KATEGORI"}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||||
{article.title}
|
{article.title}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,176 @@
|
||||||
// components/Footer.tsx
|
// components/Footer.tsx
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Facebook, Twitter, Instagram, Youtube } from "lucide-react";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#ECEFF5] pt-20 pb-10 w-full">
|
<footer className=" text-[#FFFFFFCC] text-sm font-sans ">
|
||||||
<div className="max-w-screen-xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 ">
|
<div className="max-w-7xl mx-auto py-10 bg-[#09282C] px-8">
|
||||||
{/* Logo */}
|
{/* Top Menu Links */}
|
||||||
<div className="flex justify-center md:justify-end">
|
<div className="flex flex-col md:flex-row justify-center md:justify-between gap-3">
|
||||||
<Image
|
{/* <div className="w-full md:w-2/12">
|
||||||
src="/mikul-news-logo.png"
|
<p className="text-sm text-gray-400 mt-5">
|
||||||
alt="Logo"
|
© 2025{" "}
|
||||||
width={230}
|
<span className="text-xs text-white font-semibold">JNews</span>-
|
||||||
height={230}
|
Premium WordPress news & magazine theme by{" "}
|
||||||
className="object-contain"
|
<span className="text-white font-semibold">Jegtheme</span>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subscribe Box */}
|
|
||||||
<div className="flex justify-center md:justify-end">
|
|
||||||
<div className=" p-8 w-full md:w-[420px]">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-800 leading-snug">
|
|
||||||
Subscribe us to get <br />
|
|
||||||
the latest news!
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button className="mt-4 bg-green-600 hover:bg-green-500 text-black px-6 py-3 rounded-md font-medium">
|
|
||||||
SIGN UP
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-8 mt-16 text-gray-600 text-sm">
|
|
||||||
<a href="#">About Us</a>
|
|
||||||
<a href="#">Contact</a>
|
|
||||||
<a href="#">Kode Etik Jurnalistik</a>
|
|
||||||
<a href="#">Kebijakan Privasi</a>
|
|
||||||
<a href="#">Disclaimer</a>
|
|
||||||
<a href="#">Pedoman Media Siber</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center gap-8 mt-8 text-gray-700">
|
|
||||||
<Facebook className="w-5 h-5 cursor-pointer" />
|
|
||||||
<Twitter className="w-5 h-5 cursor-pointer" />
|
|
||||||
<Instagram className="w-5 h-5 cursor-pointer" />
|
|
||||||
<Youtube className="w-5 h-5 cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-start text-gray-500 text-sm mt-8 pl-5">
|
|
||||||
© 2025 Mikul News - All Rights Reserved.
|
|
||||||
</p>
|
</p>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex items-center overflow-hidden mb-4 py-6 px-8">
|
||||||
|
<Image
|
||||||
|
src="/mikul.png"
|
||||||
|
alt="Background"
|
||||||
|
width={272}
|
||||||
|
height={90}
|
||||||
|
className="w-full md:w-[272px] h-[90px] object-cover border"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<div className="flex flex-col justify-center md:justify-end gap-2 md:gap-3 text-xs text-[#FFFFFF]">
|
||||||
|
<p className="text-xs font-bold text-red-600 mb-2 md:mb-0 w-10/12 text-start">
|
||||||
|
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>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,222 +11,158 @@ 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: { title: string }[];
|
categories: {
|
||||||
files: { fileUrl: string; file_alt: string }[];
|
title: string;
|
||||||
|
}[];
|
||||||
|
files: {
|
||||||
|
file_url: string;
|
||||||
|
file_alt: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Header() {
|
export default function HeroNewsSection() {
|
||||||
|
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(() => {
|
||||||
const fetchArticles = async () => {
|
initState();
|
||||||
|
}, [page, showData, startDateValue, selectedCategories]);
|
||||||
|
|
||||||
|
async function initState() {
|
||||||
|
// loading();
|
||||||
const req = {
|
const req = {
|
||||||
limit: "10",
|
limit: showData,
|
||||||
page: 1,
|
page,
|
||||||
search: "",
|
search,
|
||||||
categorySlug: "",
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
sort: "desc",
|
sort: "desc",
|
||||||
isPublish: true,
|
isPublish: true,
|
||||||
sortBy: "created_at",
|
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 {
|
||||||
fetchArticles();
|
// close();
|
||||||
}, []);
|
}
|
||||||
|
}
|
||||||
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 px-4">
|
<section className="max-w-7xl mx-auto bg-white">
|
||||||
{/* FLASH STRIP */}
|
<div className="flex items-center bg-[#F2F4F3] w-full overflow-hidden mb-4 py-6 px-8">
|
||||||
<div className="flex items-center justify-between mt-6 mb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h4 className="text-green-600 font-semibold">Flash</h4>
|
|
||||||
<span className="text-red-500">⚡</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 hidden md:block">LOAD MORE ➜</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto no-scrollbar py-2">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
{flashArticles.map((item) => (
|
|
||||||
<Link
|
|
||||||
href={`/details/${item.slug}`}
|
|
||||||
key={`flash-${item.id}`}
|
|
||||||
className="min-w-[200px] md:min-w-[220px] bg-gray-800 rounded-lg overflow-hidden relative shadow"
|
|
||||||
>
|
|
||||||
<div className="relative w-[200px] md:w-[220px] h-[140px]">
|
|
||||||
<Image
|
<Image
|
||||||
src={
|
src="/mikul.png"
|
||||||
item.thumbnailUrl ||
|
alt="Background"
|
||||||
item.files?.[0]?.fileUrl ||
|
width={272}
|
||||||
"/placeholder.jpg"
|
height={90}
|
||||||
}
|
className="w-full md:w-[272px] h-[90px] object-cover border"
|
||||||
alt={item.title}
|
priority
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* dark overlay with text */}
|
<div className="pb-5">
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent text-white">
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-2 m-8 ">
|
||||||
<p className="text-xs line-clamp-2">{item.title}</p>
|
{articles.length > 0 && (
|
||||||
<div className="flex items-center justify-between mt-2 text-[11px] text-gray-300">
|
<div className="md:col-span-2 lg:col-span-3 relative">
|
||||||
<span className="text-yellow-300 bg-black/30 px-1 rounded-sm">
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
{item.categoryName ||
|
|
||||||
item.categories?.[0]?.title ||
|
|
||||||
"Berita"}
|
|
||||||
</span>
|
|
||||||
<span>●</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* play icon */}
|
|
||||||
<div className="absolute top-3 right-3 w-8 h-8 bg-white/80 rounded-full flex items-center justify-center">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-black"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M8 5v14l11-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Layout */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-[2.2fr_1fr] gap-6">
|
|
||||||
{/* LEFT SIDE – MAIN ARTICLE */}
|
|
||||||
{mainArticle ? (
|
|
||||||
<div className="relative h-[500px] w-full rounded-xl overflow-hidden shadow-md">
|
|
||||||
<Link href={`/details/${mainArticle.slug}`}>
|
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
mainArticle.thumbnailUrl ||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
mainArticle.files?.[0]?.fileUrl ||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
"/placeholder.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
|
alt={articles[0].title}
|
||||||
fill
|
width={800}
|
||||||
className="object-cover"
|
height={500}
|
||||||
|
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">
|
||||||
{/* White Card Overlay */}
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
||||||
<div className="absolute bottom-6 left-6 bg-white bg-opacity-95 backdrop-blur-sm p-6 shadow-lg max-w-lg">
|
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
<span className="text-[11px] bg-green-700 text-white px-2 py-1 rounded-sm">
|
|
||||||
{mainArticle.categoryName ||
|
|
||||||
mainArticle.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">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mt-2 leading-snug">
|
{articles[0].title}
|
||||||
{mainArticle.title}
|
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="text-white text-xs flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 text-gray-600 text-xs mt-3">
|
{articles[0].createdByName} -{" "}
|
||||||
<span>
|
<svg
|
||||||
By{" "}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{mainArticle.customCreatorName ||
|
width="16"
|
||||||
mainArticle.createdByName ||
|
height="16"
|
||||||
"Admin"}
|
viewBox="0 0 24 24"
|
||||||
</span>
|
>
|
||||||
<span>•</span>
|
<g fill="none">
|
||||||
<span>
|
<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" />
|
||||||
{new Date(mainArticle.publishedAt).toLocaleDateString(
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>{" "}
|
||||||
|
{new Date(articles[0].createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
day: "2-digit",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</span>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">Loading...</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* RIGHT SIDE – RECENT POSTS */}
|
<div className="md:col-span-1 lg:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<div>
|
{articles.slice(1, 5).map((article, index) => (
|
||||||
<h3 className="text-lg font-semibold mb-3">Recent Posts</h3>
|
<div key={index} className="relative">
|
||||||
|
<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={
|
||||||
item.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
item.files?.[0]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/placeholder.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={item.title}
|
alt={article.title}
|
||||||
fill
|
width={400}
|
||||||
className="object-cover"
|
height={240}
|
||||||
|
className="w-full h-56 object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||||
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
||||||
<div className="flex flex-col">
|
{article?.categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
<p className="text-sm font-semibold line-clamp-2">
|
|
||||||
{item.title}
|
|
||||||
</p>
|
|
||||||
<span className="text-xs text-gray-500 mt-1">
|
|
||||||
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
|
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||||
|
{article.title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* LOAD MORE */}
|
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8">
|
||||||
<div className="flex justify-center my-6">
|
|
||||||
<button className="text-gray-600 text-sm flex items-center gap-2 border-b pb-1">
|
|
||||||
<span>LOAD MORE</span>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
|
||||||
<path
|
|
||||||
d="M12 5v14M5 12h14"
|
|
||||||
stroke="#9CA3AF"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* KOLOM PPS BOTTOM BANNER */}
|
|
||||||
<div className="relative h-[140px] w-full overflow-hidden rounded-none my-5 border rounded-md">
|
|
||||||
<Image
|
<Image
|
||||||
src="/image-kolom.png"
|
src="/image-kolom.png"
|
||||||
alt="Kolom PPS Bottom Banner"
|
alt="Berita Utama"
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -12,15 +12,13 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -38,7 +36,7 @@ export default function HeaderHealth() {
|
||||||
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[]>([]);
|
||||||
const [showData, setShowData] = useState("100");
|
const [showData, setShowData] = useState("5");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
|
@ -77,15 +75,6 @@ export default function HeaderHealth() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthArticles = articles.filter((article) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainArticle = healthArticles[0];
|
|
||||||
const otherArticles = healthArticles.slice(1, 3);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-7xl mx-auto bg-white">
|
<section className="max-w-7xl mx-auto bg-white">
|
||||||
<div className="flex flex-col items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
<div className="flex flex-col items-start bg-[#F2F4F3] w-full overflow-hidden py-6 px-8 gap-3">
|
||||||
|
|
@ -108,28 +97,30 @@ export default function HeaderHealth() {
|
||||||
|
|
||||||
<div className="pb-5">
|
<div className="pb-5">
|
||||||
<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 && (
|
{articles.length > 0 && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/details/${mainArticle.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
src={
|
||||||
alt={mainArticle.title}
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
"/default-image.jpg"
|
||||||
|
}
|
||||||
|
alt={articles[0].title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
className="w-full h-full max-h-[460px] 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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent p-6 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
||||||
{mainArticle.categories?.[0]?.title || "TANPA KATEGORI"}
|
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
||||||
</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">
|
<h2 className="text-sm md:text-xl lg:text-2xl font-bold text-white leading-snug mb-2 w-full md:w-9/12">
|
||||||
{mainArticle.title}
|
{articles[0].title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle?.customCreatorName ||
|
{articles[0].createdByName} -{" "}
|
||||||
mainArticle.createdByName}{" "}
|
{new Date(articles[0].createdAt).toLocaleDateString(
|
||||||
-{" "}
|
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|
@ -144,13 +135,13 @@ export default function HeaderHealth() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{articles.slice(1, 3).map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/details/${article.slug}`}>
|
<Link href={`/detail/${article?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
@ -160,7 +151,7 @@ export default function HeaderHealth() {
|
||||||
/>
|
/>
|
||||||
<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 className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
<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"}
|
{article.categoryName || "TANPA KATEGORI"}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||||
{article.title}
|
{article.title}
|
||||||
|
|
@ -171,6 +162,15 @@ export default function HeaderHealth() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Berita Utama"
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,441 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { getListArticle } from "@/service/article";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import news from "../news";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { getAdvertise } from "@/service/advertisement";
|
|
||||||
|
|
||||||
type Article = {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
categoryName: string;
|
|
||||||
createdAt: string;
|
|
||||||
slug: string;
|
|
||||||
createdByName: string;
|
|
||||||
customCreatorName: string;
|
|
||||||
thumbnailUrl: string;
|
|
||||||
categories: {
|
|
||||||
title: string;
|
|
||||||
}[];
|
|
||||||
files: {
|
|
||||||
fileUrl: string;
|
|
||||||
file_alt: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const slugToLabel = (slug: string) => {
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
development: "Pembangunan",
|
|
||||||
health: "Kesehatan",
|
|
||||||
"citizen-news": "Berita Warga",
|
|
||||||
};
|
|
||||||
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() {
|
|
||||||
const [activeTab, setActiveTab] = useState("comments");
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPage, setTotalPage] = useState(1);
|
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
|
||||||
const [showData, setShowData] = useState("100");
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pathname = usePathname();
|
|
||||||
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 categoryLabel = slugToLabel(categorySlug);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initState();
|
|
||||||
}, [page, showData, startDateValue, selectedCategories, activeTab]);
|
|
||||||
|
|
||||||
async function initState() {
|
|
||||||
let sortBy = "created_at";
|
|
||||||
if (activeTab === "comments") sortBy = "comment_count";
|
|
||||||
if (activeTab === "trending") sortBy = "view_count";
|
|
||||||
|
|
||||||
// loading();
|
|
||||||
const req = {
|
|
||||||
limit: showData,
|
|
||||||
page: 1,
|
|
||||||
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);
|
|
||||||
} finally {
|
|
||||||
// close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const kesehatanArticles = articles.filter((article) =>
|
|
||||||
article.categories?.some((category) =>
|
|
||||||
category.title?.toLowerCase().includes("berita warga")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const itemsPerPage = 2;
|
|
||||||
const calculatedTotalPage = Math.ceil(
|
|
||||||
kesehatanArticles.length / itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginatedArticles = kesehatanArticles.slice(
|
|
||||||
(page - 1) * itemsPerPage,
|
|
||||||
page * itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
function truncateText(text: string, wordLimit: number) {
|
|
||||||
const words = text.split(" ");
|
|
||||||
if (words.length <= wordLimit) return text;
|
|
||||||
return words.slice(0, wordLimit).join(" ") + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white grid grid-cols-1 lg:grid-cols-3 gap-6 py-10 px-8">
|
|
||||||
<div className="lg:col-span-2 space-y-10">
|
|
||||||
{paginatedArticles.map((item) => (
|
|
||||||
<div key={item.id}>
|
|
||||||
<Link
|
|
||||||
className="flex flex-col md:flex-row gap-6"
|
|
||||||
href={`/details/${item?.slug}`}
|
|
||||||
>
|
|
||||||
<div className="relative w-full md:w-1/2 h-64">
|
|
||||||
<Image
|
|
||||||
src={item.thumbnailUrl || "/placeholder.png"}
|
|
||||||
alt={item.title}
|
|
||||||
fill
|
|
||||||
className="object-cover rounded"
|
|
||||||
/>
|
|
||||||
<span className="absolute top-3 left-3 bg-yellow-400 text-black px-3 py-1 text-xs font-bold">
|
|
||||||
{item.categories[0]?.title || "Pembangunan"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-xl font-bold text-[#16324F] hover:text-green-600 cursor-pointer">
|
|
||||||
{item.title}
|
|
||||||
</h2>
|
|
||||||
<div className="text-sm text-gray-600 mt-2">
|
|
||||||
BY{" "}
|
|
||||||
<span className="text-green-600 font-semibold">
|
|
||||||
{item?.customCreatorName || item.createdByName || "Admin"}
|
|
||||||
</span>{" "}
|
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
|
||||||
</div>
|
|
||||||
<p className="mt-3 text-gray-700">
|
|
||||||
{truncateText(item.description, 20)}
|
|
||||||
</p>
|
|
||||||
<button className="mt-4 px-4 py-2 border border-gray-400 text-gray-700 hover:bg-black hover:text-white transition">
|
|
||||||
READ MORE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
<div className="flex items-center justify-center gap-2 mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
|
|
||||||
disabled={page === 1}
|
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{Array.from({ length: calculatedTotalPage }, (_, i) => i + 1)
|
|
||||||
.filter((p) => {
|
|
||||||
return (
|
|
||||||
p === 1 ||
|
|
||||||
p === calculatedTotalPage ||
|
|
||||||
(p >= page - 1 && p <= page + 1)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((p, idx, arr) => {
|
|
||||||
const prev = arr[idx - 1];
|
|
||||||
const showEllipsis = prev && p - prev > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={p} className="flex items-center">
|
|
||||||
{showEllipsis && <span className="px-2">...</span>}
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(p)}
|
|
||||||
className={`px-3 py-1 ${
|
|
||||||
page === p ? "bg-green-600 text-white" : "border"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{p}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setPage((prev) => Math.min(prev + 1, calculatedTotalPage))
|
|
||||||
}
|
|
||||||
disabled={page === calculatedTotalPage}
|
|
||||||
className="px-3 py-1 border"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="w-full h-[400px] relative">
|
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
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>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Connect with us</h3>
|
|
||||||
<div className="h-1 w-20 bg-green-600 mb-4"></div>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="bg-blue-500 text-white text-center p-3">
|
|
||||||
<p className="text-xl font-bold">138</p>
|
|
||||||
<p className="text-sm">Followers</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-600 text-white text-center p-3">
|
|
||||||
<p className="text-xl font-bold">205k</p>
|
|
||||||
<p className="text-sm">Subscribers</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-yellow-400 text-black text-center p-3">
|
|
||||||
<p className="text-xl font-bold">23.9k</p>
|
|
||||||
<p className="text-sm">Followers</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex gap-4 border-b">
|
|
||||||
{["trending", "comments", "latest"].map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
className={`pb-2 capitalize ${
|
|
||||||
activeTab === tab
|
|
||||||
? "border-b-2 border-green-600 text-green-600"
|
|
||||||
: "text-gray-600"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setPage(1);
|
|
||||||
setActiveTab(tab);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
{articles.slice(0, 5).map((item) => (
|
|
||||||
<div key={item.id} className="flex gap-3 items-center">
|
|
||||||
<Link
|
|
||||||
className="flex gap-3 items-center"
|
|
||||||
href={`/details/${item?.slug}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
|
||||||
alt={item.title}
|
|
||||||
width={80}
|
|
||||||
height={60}
|
|
||||||
className="object-cover rounded"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-sm">{item.title}</h3>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{new Date(item.createdAt).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="">
|
|
||||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
|
||||||
Recommended
|
|
||||||
</h2>
|
|
||||||
<div className=" w-full">
|
|
||||||
<div className="relative w-full aspect-video mb-5">
|
|
||||||
<Link href={`/details/${articles[0]?.slug}`}>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
articles[0]?.thumbnailUrl ||
|
|
||||||
articles[0]?.files?.[0]?.fileUrl ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={"articles[0]?.title"}
|
|
||||||
fill
|
|
||||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/30" />
|
|
||||||
<div className="absolute bottom-0.5 left-2 text-white">
|
|
||||||
<h3 className=" font-semibold text-base mb-1">
|
|
||||||
{articles[0]?.title}
|
|
||||||
</h3>
|
|
||||||
<p className=" text-xs mb-2 flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="none">
|
|
||||||
<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" />
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>{" "}
|
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-5">
|
|
||||||
{articles?.slice(1, 4).map((article, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Link
|
|
||||||
className="flex gap-3"
|
|
||||||
href={`/details/${article?.slug}`}
|
|
||||||
>
|
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
article?.thumbnailUrl ||
|
|
||||||
article?.files?.[0]?.fileUrl ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={"article?.title"}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold mb-3">
|
|
||||||
{article?.title}
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-gray-500 flex gap-2 items-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="none">
|
|
||||||
<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" />
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>{" "}
|
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
|
||||||
"id-ID",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -171,17 +171,14 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -240,7 +237,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">
|
||||||
|
|
@ -253,12 +250,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={`/details/${article?.slug}`}>
|
<Link href={`/detail/${article?.id}`}>
|
||||||
<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]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article?.title || "No title"}
|
alt={article?.title || "No title"}
|
||||||
|
|
@ -283,7 +280,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?.customCreatorName || article.createdByName}
|
{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">
|
||||||
|
|
@ -302,18 +299,14 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(article?.publishedAt)
|
{new Date(article?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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"
|
||||||
|
|
@ -374,11 +367,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={`/details/${articles[0]?.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.fileUrl ||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -406,18 +399,14 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
@ -428,13 +417,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={`/details/${article?.slug}`}
|
href={`/detail/${article?.id}`}
|
||||||
>
|
>
|
||||||
<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]?.fileUrl ||
|
article?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
@ -461,18 +450,14 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -176,17 +176,15 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -251,9 +249,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={`/details/${articles[0]?.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={articles[0]?.files?.[0]?.fileUrl || "/nodata.png"}
|
src={articles[0]?.files?.[0]?.file_url || "/nodata.png"}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||||
|
|
@ -279,18 +277,14 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
@ -301,7 +295,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={`/details/${article?.slug}`}
|
href={`/detail/${article?.id}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -330,18 +324,14 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
@ -352,10 +342,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={`/details/${articles[0]?.slug}`}>
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.files?.[0]?.fileUrl || "/default-image.jpg"
|
articles[0]?.files?.[0]?.file_url || "/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
fill
|
fill
|
||||||
|
|
@ -382,18 +372,14 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
@ -404,7 +390,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={`/details/${article?.slug}`}
|
href={`/detail/${article?.id}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -433,18 +419,14 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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>
|
||||||
|
|
@ -492,10 +474,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={`/details/${post?.slug}`}>
|
<Link href={`/detail/${post?.id}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex gap-4 ${
|
className={`flex gap-4 ${
|
||||||
post.files?.[0]?.fileUrl
|
post.files?.[0]?.file_url
|
||||||
? "flex-col md:flex-row"
|
? "flex-col md:flex-row"
|
||||||
: "flex-col"
|
: "flex-col"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -519,18 +501,14 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.publishedAt)
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
.toLocaleString("id-ID", {
|
"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"
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,64 @@
|
||||||
"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";
|
||||||
|
|
||||||
export default function Navbar() {
|
const Navbar = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const toggleMenu = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isActive = (href: any) => {
|
|
||||||
return pathname === href || pathname.startsWith(href + "/");
|
const navLinks = [
|
||||||
|
{ 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 (
|
||||||
<div className="w-full bg-white py-4 border-b">
|
<nav className="w-full bg-white shadow-md">
|
||||||
<div className="max-w-screen-xl mx-auto flex flex-col justify-between px-4">
|
<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]">
|
||||||
{/* Left: Logo */}
|
<div className="flex items-center gap-4 w-full md:w-auto mx-3 md:mx-5">
|
||||||
<div className="flex flex-row justify-between mb-3">
|
<div className="flex gap-3 text-xs mx-3 text-white">
|
||||||
<div className="flex items-center">
|
<Link href="/about" className="hover:text-yellow-400 ">
|
||||||
<Image
|
About
|
||||||
src="/mikul-news-logo.png"
|
</Link>
|
||||||
alt="Kritik Tajam Logo"
|
<Link href="/advertise" className="hover:text-yellow-400">
|
||||||
width={140}
|
Advertise
|
||||||
height={100}
|
</Link>
|
||||||
/>
|
<Link href="/privacy" className="hover:text-yellow-400">
|
||||||
|
Privacy & Policy
|
||||||
|
</Link>
|
||||||
|
<Link href="/contact" className="hover:text-yellow-400">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
</div>
|
||||||
{/* Social Icons */}
|
|
||||||
<div className="hidden md:flex items-center gap-5 text-black text-xl">
|
<div className="flex items-center gap-5 text-sm whitespace-nowrap text-white mx-5">
|
||||||
<Link href="#">
|
<div className="hidden md:block min-w-fit whitespace-nowrap text-white text-xs">
|
||||||
|
{new Date().toLocaleDateString("id-ID", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 text-white text-lg">
|
||||||
|
<Link href="#" aria-label="Facebook">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width="15"
|
||||||
|
height="15"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -39,11 +67,11 @@ export default function Navbar() {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="#" aria-label="Twitter">
|
||||||
<Link href="#">
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width="15"
|
||||||
|
height="15"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -52,119 +80,150 @@ export default function Navbar() {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="#" aria-label="Google" className="text-[#F5F5F5]">
|
||||||
<Link href="#">
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width="15"
|
||||||
|
height="15"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
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"
|
// 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>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="#" aria-label="Pinterest">
|
||||||
<Link href="#">
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width="15"
|
||||||
|
height="15"
|
||||||
|
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="15"
|
||||||
|
height="15"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
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"
|
// 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>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/auth"
|
||||||
|
className="hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Lock className="w-3 h-3" />
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
</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>
|
|
||||||
|
|
||||||
<Link
|
<div className="bg-[#31942E] text-white">
|
||||||
href="/category/citizen-news"
|
<div className="flex items-start justify-start md:justify-center md:items-center px-4 py-3 md:px-8 md:py-4">
|
||||||
className={
|
{/* Toggle Menu (Mobile Only) */}
|
||||||
isActive("/category/citizen-news")
|
|
||||||
? "text-green-500 underline"
|
|
||||||
: "text-black hover:text-green-500"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Berita Warga
|
|
||||||
</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
|
<button
|
||||||
// onClick={() => document.documentElement.classList.toggle("dark")}
|
className="md:hidden flex items-center"
|
||||||
className="w-10 h-5 rounded-full bg-gray-300 dark:bg-gray-700 relative transition-all"
|
onClick={toggleMenu}
|
||||||
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
<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>
|
<Menu className="h-6 w-6 text-white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* BURGER BUTTON (mobile menu) */}
|
{/* Navigation Links (Desktop Only) */}
|
||||||
<button className="md:hidden p-2 rounded-lg border">
|
<div className="hidden md:flex items-center md:gap-x-32 font-semibold text-sm">
|
||||||
<svg
|
{navLinks.map((link, idx) => (
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<Link
|
||||||
fill="none"
|
key={idx}
|
||||||
viewBox="0 0 24 24"
|
href={link.href}
|
||||||
strokeWidth={2}
|
className={`pl-4 pr-4 ${
|
||||||
stroke="currentColor"
|
isActive(link.href)
|
||||||
className="w-6 h-6"
|
? "text-yellow-400"
|
||||||
|
: "hover:text-yellow-400 text-white"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<path
|
{link.label}
|
||||||
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>
|
</Link>
|
||||||
|
))}
|
||||||
|
<Search className="w-5 h-5 hover:text-yellow-400 cursor-pointer ml-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="md:hidden px-4 pb-4 flex flex-col gap-2 font-semibold text-sm bg-[#31942E]">
|
||||||
|
{navLinks.map((link, idx) => (
|
||||||
|
<Link
|
||||||
|
key={idx}
|
||||||
|
href={link.href}
|
||||||
|
className={`${
|
||||||
|
isActive(link.href)
|
||||||
|
? "text-yellow-400"
|
||||||
|
: "hover:text-yellow-400 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"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";
|
||||||
|
|
@ -10,36 +9,24 @@ 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: {
|
||||||
fileUrl: string;
|
file_url: 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);
|
||||||
const [posts, setPosts] = useState<postsData[]>([]);
|
const [posts, setPosts] = useState<postsData[]>([]);
|
||||||
const [showData, setShowData] = useState("100");
|
const [showData, setShowData] = useState("5");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
|
@ -47,36 +34,6 @@ 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]);
|
||||||
|
|
@ -106,148 +63,41 @@ export default function Beranda() {
|
||||||
<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">Pembangunan</h2>
|
<h2 className="text-sm font-bold">Pembangunan</h2>
|
||||||
<div className="flex flex-wrap gap-2 text-xs text-gray-600">
|
<div className="flex flex-wrap gap-2 text-xs text-gray-600">
|
||||||
<Link href={"/category/development"}>
|
|
||||||
<button className="hover:text-green-500">ALL</button>
|
<button className="hover:text-green-500">ALL</button>
|
||||||
</Link>
|
<button className="hover:text-green-500">Pembangunan</button>
|
||||||
<button className="hover:text-green-500 text-green-500">
|
|
||||||
Pembangunan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 pt-4 ">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 pt-4 ">
|
||||||
{posts
|
{posts.slice(1, 5).map((posts, index) => (
|
||||||
.filter((post) =>
|
|
||||||
post.categories?.some(
|
|
||||||
(category) => category.title.toLowerCase() === "pembangunan",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
|
||||||
.map((post, index) => (
|
|
||||||
<div key={index} className="bg-white overflow-hidden">
|
<div key={index} className="bg-white overflow-hidden">
|
||||||
<Link href={`/details/${post?.slug}`}>
|
<Link href={`/detail/${posts?.id}`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
post.thumbnailUrl ||
|
posts.thumbnailUrl ||
|
||||||
post?.files?.[0]?.fileUrl ||
|
posts?.files?.[0]?.file_url ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={post.title}
|
alt={posts.title}
|
||||||
width={500}
|
width={500}
|
||||||
height={300}
|
height={300}
|
||||||
className="w-full h-52 md:h-56 object-cover"
|
className="w-full h-52 md:h-56 object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/20" />
|
<div className="absolute inset-0 bg-black/20" />
|
||||||
<span className="absolute top-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
<span className="absolute top-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
||||||
{post.categories?.[0]?.title}
|
{posts.categories?.[0]?.title}
|
||||||
</span>
|
</span>
|
||||||
<div className="p-3 md:p-2 absolute bottom-1 left-1 text-white">
|
<div className="p-3 md:p-2 absolute bottom-1 left-1 text-white">
|
||||||
<h3 className="font-bold text-sm md:text-base leading-snug mb-1">
|
<h3 className="font-bold text-sm md:text-base leading-snug mb-1">
|
||||||
{post.title}
|
{posts.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<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 className=" ">
|
||||||
{post?.customCreatorName || post.createdByName} -{" "}
|
{posts.createdByName} -{" "}
|
||||||
{new Date(post.publishedAt)
|
{new Date(posts.createdAt).toLocaleDateString("id-ID", {
|
||||||
.toLocaleString("id-ID", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
timeZone: "Asia/Jakarta",
|
|
||||||
})
|
|
||||||
.replace("pukul ", "")}{" "}
|
|
||||||
WIB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
|
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
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 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>
|
|
||||||
<div className="flex flex-wrap gap-2 text-xs text-gray-600">
|
|
||||||
<Link href={"/category/health"}>
|
|
||||||
<button className="hover:text-green-500">ALL</button>
|
|
||||||
</Link>
|
|
||||||
<button className="hover:text-green-500 text-green-500">
|
|
||||||
Kesehatan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 pt-4 ">
|
|
||||||
{posts
|
|
||||||
.filter((post) =>
|
|
||||||
post.categories?.some(
|
|
||||||
(category) => category.title.toLowerCase() === "kesehatan",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
|
||||||
.map((post, index) => (
|
|
||||||
<div key={index} className="bg-white overflow-hidden">
|
|
||||||
<Link href={`/details/${post?.slug}`}>
|
|
||||||
<div className="relative">
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
post.thumbnailUrl ||
|
|
||||||
post?.files?.[0]?.fileUrl ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={post.title}
|
|
||||||
width={500}
|
|
||||||
height={300}
|
|
||||||
className="w-full h-52 md:h-56 object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/20" />
|
|
||||||
<span className="absolute top-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
|
||||||
{post.categories?.[0]?.title}
|
|
||||||
</span>
|
|
||||||
<div className="p-3 md:p-2 absolute bottom-1 left-1 text-white">
|
|
||||||
<h3 className="font-bold text-sm md:text-base leading-snug mb-1">
|
|
||||||
{post.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="text-xs flex items-center gap-2">
|
|
||||||
<Clock className="w-3 h-3" />
|
|
||||||
<span>
|
|
||||||
{post?.customCreatorName || post.createdByName} -{" "}
|
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|
@ -260,33 +110,13 @@ export default function Beranda() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
|
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
||||||
{bannerAd ? (
|
|
||||||
<a
|
|
||||||
href={bannerAd.redirectLink}
|
|
||||||
target="_blank"
|
|
||||||
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
|
<Image
|
||||||
src="/image-kolom.png"
|
src="/image-kolom.png"
|
||||||
alt="Berita Utama"
|
alt="Berita Utama"
|
||||||
width={1200}
|
fill
|
||||||
height={188}
|
className="object-cover"
|
||||||
className="object-contain w-full h-[188px]"
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -106,7 +106,7 @@ export default function DashboardContainer() {
|
||||||
|
|
||||||
async function initState() {
|
async function initState() {
|
||||||
const req = {
|
const req = {
|
||||||
limit: "5",
|
limit: "4",
|
||||||
page: page,
|
page: page,
|
||||||
search: "",
|
search: "",
|
||||||
};
|
};
|
||||||
|
|
@ -207,15 +207,11 @@ 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">
|
<p className="text-2xl font-bold text-blue-600">{summary?.totalToday}</p>
|
||||||
{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">
|
<p className="text-2xl font-bold text-purple-600">{summary?.totalThisWeek}</p>
|
||||||
{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>
|
||||||
|
|
@ -238,9 +234,7 @@ export default function DashboardContainer() {
|
||||||
<DashboardSpeecIcon />
|
<DashboardSpeecIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-slate-800">{summary?.totalAll}</p>
|
||||||
{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>
|
||||||
|
|
@ -258,9 +252,7 @@ export default function DashboardContainer() {
|
||||||
<DashboardConnectIcon />
|
<DashboardConnectIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-slate-800">{summary?.totalViews}</p>
|
||||||
{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>
|
||||||
|
|
@ -278,9 +270,7 @@ export default function DashboardContainer() {
|
||||||
<DashboardShareIcon />
|
<DashboardShareIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-slate-800">{summary?.totalShares}</p>
|
||||||
{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>
|
||||||
|
|
@ -298,9 +288,7 @@ export default function DashboardContainer() {
|
||||||
<DashboardCommentIcon size={40} />
|
<DashboardCommentIcon size={40} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-slate-800">{summary?.totalComments}</p>
|
||||||
{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>
|
||||||
|
|
@ -317,20 +305,13 @@ 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">
|
<h3 className="text-lg font-semibold text-slate-800">Analytics Overview</h3>
|
||||||
Analytics Overview
|
|
||||||
</h3>
|
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<label
|
<label key={option.value} className="flex items-center space-x-2">
|
||||||
key={option.value}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={analyticsView.includes(option.value)}
|
checked={analyticsView.includes(option.value)}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => handleChange(option.value, checked as boolean)}
|
||||||
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>
|
||||||
|
|
@ -354,14 +335,12 @@ 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">
|
<h3 className="text-lg font-semibold text-slate-800">Recent Articles</h3>
|
||||||
Recent Articles
|
<Link href="/admin/article/create">
|
||||||
</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">
|
||||||
|
|
|
||||||
|
|
@ -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, formatDate } from "@/utils/global";
|
import { convertDateFormat } 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,7 +46,6 @@ 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" },
|
||||||
|
|
@ -54,18 +53,17 @@ 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: "customCreatorName" },
|
{ name: "Kreator", uid: "createdByName" },
|
||||||
{ 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: "customCreatorName" },
|
{ name: "Kreator", uid: "createdByName" },
|
||||||
{ name: "Status", uid: "publishStatus" },
|
{ name: "Status", uid: "isPublish" },
|
||||||
{ name: "Aksi", uid: "actions" },
|
{ name: "Aksi", uid: "actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -86,10 +84,7 @@ 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 [selectedCategoryId, setSelectedCategoryId] = useState<any>("");
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
const [selectedSource, setSelectedSource] = useState<any>("");
|
|
||||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
|
||||||
const [dateRange, setDateRange] = useState<any>({
|
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
|
|
@ -103,45 +98,24 @@ 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,
|
||||||
category: selectedCategoryId || "",
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
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();
|
||||||
|
|
@ -199,11 +173,11 @@ export default function ArticleTable() {
|
||||||
initState();
|
initState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyUrlArticle = async (slug: any) => {
|
const copyUrlArticle = async (id: number, slug: string) => {
|
||||||
const url =
|
const url =
|
||||||
`${window.location.protocol}//${window.location.host}` +
|
`${window.location.protocol}//${window.location.host}` +
|
||||||
"/details/" +
|
"/news/detail/" +
|
||||||
`${slug}`;
|
`${id}-${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");
|
||||||
|
|
@ -218,16 +192,7 @@ export default function ArticleTable() {
|
||||||
const cellValue = article[columnKey as keyof any];
|
const cellValue = article[columnKey as keyof any];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "customCreatorName":
|
case "isPublish":
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
{article.customCreatorName &&
|
|
||||||
article.customCreatorName.trim() !== ""
|
|
||||||
? article.customCreatorName
|
|
||||||
: article.createdByName}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
case "publishStatus":
|
|
||||||
return (
|
return (
|
||||||
// <Chip
|
// <Chip
|
||||||
// className="capitalize "
|
// className="capitalize "
|
||||||
|
|
@ -239,7 +204,7 @@ export default function ArticleTable() {
|
||||||
// {article.status}
|
// {article.status}
|
||||||
// </div>
|
// </div>
|
||||||
// </Chip>
|
// </Chip>
|
||||||
<p>{article.publishStatus}</p>
|
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
||||||
);
|
);
|
||||||
case "isBanner":
|
case "isBanner":
|
||||||
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
||||||
|
|
@ -264,7 +229,7 @@ export default function ArticleTable() {
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => copyUrlArticle(article.slug)}
|
onClick={() => copyUrlArticle(article.id, article.slug)}
|
||||||
>
|
>
|
||||||
<CopyIcon className="mr-2 h-4 w-4" />
|
<CopyIcon className="mr-2 h-4 w-4" />
|
||||||
Copy Url Article
|
Copy Url Article
|
||||||
|
|
@ -280,6 +245,8 @@ export default function ArticleTable() {
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{(username === "admin-mabes" ||
|
||||||
|
Number(userId) === article.createdById) && (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/article/edit/${article.id}`}
|
href={`/admin/article/edit/${article.id}`}
|
||||||
|
|
@ -289,6 +256,7 @@ export default function ArticleTable() {
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{username === "admin-mabes" && (
|
{username === "admin-mabes" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|
@ -303,10 +271,13 @@ export default function ArticleTable() {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(username === "admin-mabes" ||
|
||||||
|
Number(userId) === article.createdById) && (
|
||||||
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
|
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
|
||||||
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
|
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,7 +287,7 @@ export default function ArticleTable() {
|
||||||
return cellValue;
|
return cellValue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[article, page],
|
[article, page]
|
||||||
);
|
);
|
||||||
|
|
||||||
let typingTimer: NodeJS.Timeout;
|
let typingTimer: NodeJS.Timeout;
|
||||||
|
|
@ -375,78 +346,36 @@ 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={selectedCategoryId}
|
value={selectedCategories}
|
||||||
onValueChange={(value) => setSelectedCategoryId(value)} // simpan ID
|
onValueChange={(value) => setSelectedCategories(value)}
|
||||||
>
|
>
|
||||||
<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.title != null)
|
?.filter((category: any) => category.slug != null)
|
||||||
.map((category: any) => (
|
.map((category: any) => (
|
||||||
<SelectItem
|
<SelectItem key={category.slug} value={category.slug}>
|
||||||
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-[150px]">
|
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
||||||
<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
|
||||||
selectsRange
|
value={startDateValue}
|
||||||
startDate={dateRange.startDate}
|
displayFormat="DD/MM/YYYY"
|
||||||
endDate={dateRange.endDate}
|
onChange={(e: any) => setStartDateValue(e)}
|
||||||
onChange={(update: [Date | null, Date | null]) => {
|
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"
|
||||||
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 overflow-x-auto">
|
<div className="w-full mx-auto overflow-x-hidden">
|
||||||
<Table className="min-w-[1000px] w-full table-auto border text-sm">
|
<Table className="w-full table-fixed border text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{(username === "admin-mabes"
|
{(username === "admin-mabes"
|
||||||
|
|
@ -455,18 +384,7 @@ export default function ArticleTable() {
|
||||||
).map((column) => (
|
).map((column) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
className={`bg-white dark:bg-black text-black dark:text-white
|
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
||||||
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>
|
||||||
|
|
@ -483,17 +401,7 @@ export default function ArticleTable() {
|
||||||
).map((column) => (
|
).map((column) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
className={`text-black dark:text-white text-sm px-3 py-3 align-top
|
className="truncate text-black dark:text-white max-w-[200px]"
|
||||||
${
|
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
/** @type {import('next').NextConfig} */
|
import type { NextConfig } from "next";
|
||||||
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'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -27,7 +27,6 @@
|
||||||
"@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",
|
||||||
|
|
@ -39,12 +38,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": "^16.1.1",
|
"next": "15.3.4",
|
||||||
"react": "^19.2.3",
|
"react": "^19.0.0",
|
||||||
"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.2.4",
|
"react-dom": "^19.0.0",
|
||||||
"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",
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
|
|
@ -1,11 +1,6 @@
|
||||||
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 {
|
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||||
httpDeleteInterceptor,
|
|
||||||
httpGetInterceptor,
|
|
||||||
httpPostInterceptor,
|
|
||||||
httpPutInterceptor,
|
|
||||||
} from "./http-config/http-interceptor-services";
|
|
||||||
|
|
||||||
export async function getListArticle(props: PaginationRequest) {
|
export async function getListArticle(props: PaginationRequest) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -21,14 +16,13 @@ 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 || "desc"
|
sort || "asc"
|
||||||
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -46,20 +40,13 @@ 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=${
|
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${startDate || ""}&endDate=${
|
||||||
startDate || ""
|
endDate || ""
|
||||||
}&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
|
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
||||||
source || ""
|
sort || "asc"
|
||||||
}&isPublish=${isPublish !== undefined ? isPublish : ""}&sortBy=${
|
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`
|
||||||
sortBy || "created_at"
|
|
||||||
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
|
|
||||||
isBanner || ""
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,11 +81,6 @@ 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",
|
||||||
|
|
@ -106,13 +88,6 @@ 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",
|
||||||
|
|
@ -136,13 +111,6 @@ 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",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const baseURL = "https://mikulnews.com/api";
|
const baseURL = "https://dev.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"
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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://mikulnews.com/api";
|
const baseURL = "https://dev.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;
|
||||||
|
|
|
||||||
|
|
@ -94,21 +94,16 @@ 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 pathUrl = `/article-comments`;
|
const headers = token
|
||||||
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) {
|
||||||
|
|
@ -117,7 +112,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=false&articleId=${id}`;
|
const pathUrl = `/article-comments?isPublic=true&articleId=${id}`;
|
||||||
return await httpGet(pathUrl);
|
return await httpGet(pathUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
/* CKEditor Custom Styling */
|
|
||||||
|
|
||||||
/* Main editor container */
|
|
||||||
.ck.ck-editor {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbar styling */
|
|
||||||
.ck.ck-toolbar {
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-toolbar .ck-toolbar__items {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main editable area */
|
|
||||||
.ck.ck-editor__editable {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
padding: 1.5em 2em !important;
|
|
||||||
min-height: 400px;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus state */
|
|
||||||
.ck.ck-editor__editable.ck-focused {
|
|
||||||
border-color: #1a9aef;
|
|
||||||
box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content styling */
|
|
||||||
.ck.ck-editor__editable .ck-content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography improvements */
|
|
||||||
.ck.ck-editor__editable p {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable h1,
|
|
||||||
.ck.ck-editor__editable h2,
|
|
||||||
.ck.ck-editor__editable h3,
|
|
||||||
.ck.ck-editor__editable h4,
|
|
||||||
.ck.ck-editor__editable h5,
|
|
||||||
.ck.ck-editor__editable h6 {
|
|
||||||
margin: 1em 0 0.5em 0;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable h1 { font-size: 1.75em; }
|
|
||||||
.ck.ck-editor__editable h2 { font-size: 1.5em; }
|
|
||||||
.ck.ck-editor__editable h3 { font-size: 1.25em; }
|
|
||||||
.ck.ck-editor__editable h4 { font-size: 1.1em; }
|
|
||||||
.ck.ck-editor__editable h5 { font-size: 1em; }
|
|
||||||
.ck.ck-editor__editable h6 { font-size: 0.9em; }
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
.ck.ck-editor__editable ul,
|
|
||||||
.ck.ck-editor__editable ol {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable li {
|
|
||||||
margin: 0.25em 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes */
|
|
||||||
.ck.ck-editor__editable blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.75em 1em;
|
|
||||||
border-left: 4px solid #1a9aef;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
font-style: italic;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.ck.ck-editor__editable table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable table td,
|
|
||||||
.ck.ck-editor__editable table th {
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
padding: 0.5em 0.75em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable table th {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
.ck.ck-editor__editable a {
|
|
||||||
color: #1a9aef;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable a:hover {
|
|
||||||
color: #0d7cd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
.ck.ck-editor__editable pre {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1em;
|
|
||||||
margin: 1em 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable code {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
.ck.ck-editor__editable img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
.ck.ck-editor__editable hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #d1d5db;
|
|
||||||
margin: 2em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Placeholder text */
|
|
||||||
.ck.ck-editor__editable.ck-blurred:empty::before {
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
color: #9ca3af;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.ck.ck-editor__editable {
|
|
||||||
padding: 1em 1.5em !important;
|
|
||||||
font-size: 16px; /* Better for mobile */
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-toolbar {
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-toolbar .ck-toolbar__items {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode support */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.ck.ck-editor__editable {
|
|
||||||
background: #1f2937;
|
|
||||||
color: #f9fafb;
|
|
||||||
border-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable h1,
|
|
||||||
.ck.ck-editor__editable h2,
|
|
||||||
.ck.ck-editor__editable h3,
|
|
||||||
.ck.ck-editor__editable h4,
|
|
||||||
.ck.ck-editor__editable h5,
|
|
||||||
.ck.ck-editor__editable h6 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable blockquote {
|
|
||||||
background-color: #374151;
|
|
||||||
border-left-color: #1a9aef;
|
|
||||||
color: #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable pre {
|
|
||||||
background-color: #374151;
|
|
||||||
border-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-editor__editable code {
|
|
||||||
background-color: #4b5563;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
/* @import url("https://fonts.googleapis.com/css2?family=Bytesized&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); */
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
/* ========== 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
@ -15,7 +11,7 @@
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|
@ -23,19 +19,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"]
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
".next/dev/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,6 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -164,11 +164,3 @@ 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}`;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,31 @@
|
||||||
* @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 {
|
import { Image, ImageCaption, ImageInsert, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image';
|
||||||
Image,
|
import { Indent } from '@ckeditor/ckeditor5-indent';
|
||||||
ImageCaption,
|
import { Link } from '@ckeditor/ckeditor5-link';
|
||||||
ImageInsert,
|
import { List } from '@ckeditor/ckeditor5-list';
|
||||||
ImageStyle,
|
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
||||||
ImageToolbar,
|
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
||||||
ImageUpload,
|
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
||||||
} from "@ckeditor/ckeditor5-image";
|
import { SourceEditing } from '@ckeditor/ckeditor5-source-editing';
|
||||||
import { Indent } from "@ckeditor/ckeditor5-indent";
|
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
||||||
import { Link } from "@ckeditor/ckeditor5-link";
|
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
||||||
import { List } from "@ckeditor/ckeditor5-list";
|
import { Undo } from '@ckeditor/ckeditor5-undo';
|
||||||
import { MediaEmbed } from "@ckeditor/ckeditor5-media-embed";
|
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';
|
||||||
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: (
|
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)[];
|
||||||
| 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;
|
static defaultConfig: EditorConfig;
|
||||||
}
|
}
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|
|
||||||
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/cs/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/cs/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/de/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/de/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/es/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/es/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/fr/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/fr/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/it/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/it/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ja/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ja/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ko/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ko/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pl/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pl/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pt-br/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pt-br/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ru/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ru/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/tr/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/tr/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-cn/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-cn/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-tw/diagnosticMessages.generated.json
generated
vendored
Normal file
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-tw/diagnosticMessages.generated.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue