Compare commits
36 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b849ecf785 | |
|
|
b0aa58118b | |
|
|
897bd7d504 | |
|
|
f8efd7a45e | |
|
|
6ef0cc29a6 | |
|
|
58735a6343 | |
|
|
ce45ed8bdc | |
|
|
1b8ef91a72 | |
|
|
b287069d30 | |
|
|
40c79b9ae2 | |
|
|
2747e9fcd4 | |
|
|
f742115d96 | |
|
|
5bfdc59f83 | |
|
|
6270f23871 | |
|
|
19e1b7dfbd | |
|
|
50802e140b | |
|
|
21d2583120 | |
|
|
db571585c2 | |
|
|
435222a792 | |
|
|
d6d72454eb | |
|
|
36d8790cb6 | |
|
|
0351a30dcb | |
|
|
25740a50d4 | |
|
|
d780833116 | |
|
|
522245f6dc | |
|
|
6fb6e977e9 | |
|
|
87e391a5bf | |
|
|
db08a8051a | |
|
|
7ced42966b | |
|
|
85a8275eca | |
|
|
3abbe66b93 | |
|
|
a7895fc396 | |
|
|
6c28fd3a94 | |
|
|
5236d4d9d5 | |
|
|
970221ef64 | |
|
|
3a906a755b |
|
|
@ -0,0 +1,42 @@
|
||||||
|
kind: pipeline
|
||||||
|
type: ssh
|
||||||
|
name: mikul-news-build-deploy
|
||||||
|
|
||||||
|
server:
|
||||||
|
host:
|
||||||
|
from_secret: ssh_host
|
||||||
|
user:
|
||||||
|
from_secret: ssh_user
|
||||||
|
ssh_key:
|
||||||
|
from_secret: ssh_key
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: prepare repo
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
commands:
|
||||||
|
- rm -rf /opt/build/web-mikul-news
|
||||||
|
- mkdir -p /opt/build
|
||||||
|
- cd /opt/build
|
||||||
|
- git clone http://38.47.180.165:3000/medol/web-mikul-news.git
|
||||||
|
|
||||||
|
- name: build image
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
commands:
|
||||||
|
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
|
||||||
|
- cd /opt/build/web-mikul-news
|
||||||
|
- docker build -t 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH .
|
||||||
|
- docker push 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
commands:
|
||||||
|
- docker pull 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH
|
||||||
|
- docker stop web-mikul-news || true
|
||||||
|
- docker rm web-mikul-news || true
|
||||||
|
- docker run -dt -p 4600:3000 --restart always --name web-mikul-news 38.47.180.165:3000/medol/mikul-news:$DRONE_BRANCH
|
||||||
|
|
@ -7,15 +7,16 @@ build-dev:
|
||||||
when: on_success
|
when: on_success
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
image: docker:stable
|
image:
|
||||||
|
name: docker:25.0.3-cli
|
||||||
services:
|
services:
|
||||||
- name: docker:dind
|
- name: docker:25.0.3-dind
|
||||||
command: ["--insecure-registry=103.82.242.92:8900"]
|
command: ["--insecure-registry=38.47.185.86:8900"]
|
||||||
script:
|
script:
|
||||||
- docker logout
|
- docker logout
|
||||||
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
|
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
|
||||||
- docker build -t 103.82.242.92:8900/medols/web-mikul-news:dev .
|
- docker build -t 38.47.185.86:8900/medols/web-mikul-news:dev .
|
||||||
- docker push 103.82.242.92:8900/medols/web-mikul-news:dev
|
- docker push 38.47.185.86:8900/medols/web-mikul-news:dev
|
||||||
|
|
||||||
auto-deploy:
|
auto-deploy:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|
@ -26,4 +27,4 @@ auto-deploy:
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
script:
|
script:
|
||||||
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-mikul-news/build?token=autodeploymedols
|
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-mikul-news/build?token=autodeploymedols
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import Image from "next/image";
|
||||||
export default function Development() {
|
export default function Development() {
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||||
<div className="fixed top-0 left-0 w-full h-auto z-0">
|
{/* <div className="fixed top-0 left-0 w-full h-auto z-0">
|
||||||
<Image
|
<Image
|
||||||
src="/rumput.jpg"
|
src="/rumput.jpg"
|
||||||
alt="Background"
|
alt="Background"
|
||||||
|
|
@ -21,7 +21,7 @@ export default function Development() {
|
||||||
className="w-full h-auto object-cover"
|
className="w-full h-auto object-cover"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import Image from "next/image";
|
||||||
export default function Development() {
|
export default function Development() {
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||||
<div className="fixed top-0 left-0 w-full h-auto z-0">
|
{/* <div className="fixed top-0 left-0 w-full h-auto z-0">
|
||||||
<Image
|
<Image
|
||||||
src="/rumput.jpg"
|
src="/rumput.jpg"
|
||||||
alt="Background"
|
alt="Background"
|
||||||
|
|
@ -22,7 +22,7 @@ export default function Development() {
|
||||||
className="w-full h-auto object-cover"
|
className="w-full h-auto object-cover"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import DetailContent from "@/components/details/details-content";
|
||||||
|
import Footer from "@/components/landing-page/footer";
|
||||||
|
import Navbar from "@/components/landing-page/navbar";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||||
|
<div className="fixed top-0 left-0 w-full h-auto z-0">
|
||||||
|
<Image
|
||||||
|
src="/rumput.jpg"
|
||||||
|
alt="Background"
|
||||||
|
width={1450}
|
||||||
|
height={600}
|
||||||
|
className="w-full h-auto object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex-1">
|
||||||
|
<DetailContent />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/page.tsx
33
app/page.tsx
|
|
@ -1,39 +1,26 @@
|
||||||
import Author from "@/components/landing-page/author";
|
|
||||||
import Footer from "@/components/landing-page/footer";
|
import Footer from "@/components/landing-page/footer";
|
||||||
import Header from "@/components/landing-page/header";
|
import Header from "@/components/landing-page/header";
|
||||||
import Latest from "@/components/landing-page/latest";
|
|
||||||
import LatestandPopular from "@/components/landing-page/latest-and-popular";
|
|
||||||
import Navbar from "@/components/landing-page/navbar";
|
import Navbar from "@/components/landing-page/navbar";
|
||||||
import News from "@/components/landing-page/news";
|
import LatestNews from "@/components/landing-page/latest-news";
|
||||||
import { id } from "date-fns/locale";
|
import Development from "@/components/landing-page/development";
|
||||||
import Image from "next/image";
|
import OpinionNews from "@/components/landing-page/opinion-news";
|
||||||
|
import NewsTerkini from "@/components/landing-page/health";
|
||||||
|
import YouTubeSection from "@/components/landing-page/youtube-selection";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||||
{/* Background fixed tidak ikut scroll */}
|
{/* Background fixed tidak ikut scroll */}
|
||||||
<div className="fixed top-0 left-0 w-full h-auto z-0">
|
|
||||||
<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>
|
||||||
<Latest id={2} />
|
<LatestNews />
|
||||||
<News />
|
<Development />
|
||||||
<Author />
|
<OpinionNews />
|
||||||
<LatestandPopular />
|
<NewsTerkini />
|
||||||
|
<YouTubeSection />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,46 @@ import Image from "next/image";
|
||||||
import Author from "../landing-page/author";
|
import Author from "../landing-page/author";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getArticleById, getListArticle } from "@/service/article";
|
import {
|
||||||
import { close, loading } from "@/config/swal";
|
getArticleById,
|
||||||
import { useParams } from "next/navigation";
|
getArticleBySlug,
|
||||||
|
getArticleFiles,
|
||||||
|
getListArticle,
|
||||||
|
} from "@/service/article";
|
||||||
|
import { close, error, loading } from "@/config/swal";
|
||||||
|
import { useParams, usePathname } from "next/navigation";
|
||||||
|
import { Link2, MailIcon } from "lucide-react";
|
||||||
|
import { getAdvertise } from "@/service/advertisement";
|
||||||
|
import { saveActivity } from "@/service/activity-log";
|
||||||
|
import {
|
||||||
|
getArticleComment,
|
||||||
|
otpRequest,
|
||||||
|
otpValidation,
|
||||||
|
postArticleComment,
|
||||||
|
} from "@/service/master-user";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import { map } from "zod";
|
||||||
|
import { formatTextToHtmlTag } from "@/utils/global";
|
||||||
|
|
||||||
type TabKey = "trending" | "comments" | "latest";
|
type TabKey = "trending" | "comments" | "latest";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
htmlDescription: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -32,9 +53,20 @@ interface CategoryType {
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Advertise = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placement: string;
|
||||||
|
contentFileUrl: string;
|
||||||
|
redirectLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function DetailContent() {
|
export default function DetailContent() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params?.id;
|
const id = params?.id;
|
||||||
|
const slug = params?.slug;
|
||||||
|
const pathname = usePathname();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPage, setTotalPage] = useState(1);
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
|
@ -47,18 +79,82 @@ export default function DetailContent() {
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
const [detailfiles, setDetailFiles] = useState<any>([]);
|
// const [detailfiles, setDetailFiles] = useState<any>([]);
|
||||||
|
const [detailFiles, setDetailFiles] = useState<any[]>([]);
|
||||||
|
|
||||||
const [mainImage, setMainImage] = useState(0);
|
const [mainImage, setMainImage] = useState(0);
|
||||||
const [thumbnail, setThumbnail] = useState("-");
|
const [thumbnail, setThumbnail] = useState("-");
|
||||||
const [diseId, setDiseId] = useState(0);
|
const [diseId, setDiseId] = useState(0);
|
||||||
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
|
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
|
||||||
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
|
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
const [tabArticles, setTabArticles] = useState<Article[]>([]);
|
const [tabArticles, setTabArticles] = useState<Article[]>([]);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>("trending");
|
const [activeTab, setActiveTab] = useState<TabKey>("trending");
|
||||||
|
const [needOtp, setNeedOtp] = useState(false);
|
||||||
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
const { register, handleSubmit, reset, watch } = useForm();
|
||||||
|
const [commentList, setCommentList] = useState<any>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getArticleComment(String(id));
|
||||||
|
const data = res?.data?.data;
|
||||||
|
setCommentList(data);
|
||||||
|
console.log("komen", data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Gagal memuat komentar:", err);
|
||||||
|
setCommentList([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (values: any) => {
|
||||||
|
if (!needOtp) {
|
||||||
|
const res = await otpRequest(values.email, values?.name);
|
||||||
|
if (res?.error) {
|
||||||
|
error(res.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setNeedOtp(true);
|
||||||
|
} else {
|
||||||
|
const validation = await otpValidation(values.email, otpValue);
|
||||||
|
if (validation?.error) {
|
||||||
|
error("OTP Tidak Sesuai");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
commentFromName: values.name,
|
||||||
|
commentFromEmail: values.email,
|
||||||
|
articleId: Number(id),
|
||||||
|
isPublic: false,
|
||||||
|
message: values.comment,
|
||||||
|
parentId: 0,
|
||||||
|
};
|
||||||
|
const res = await postArticleComment(data);
|
||||||
|
if (res?.error) {
|
||||||
|
error(res?.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const req: any = {
|
||||||
|
activityTypeId: 5,
|
||||||
|
url: "https://dev.mikulnews/" + pathname,
|
||||||
|
articleId: Number(id),
|
||||||
|
};
|
||||||
|
|
||||||
|
const resActivity = await saveActivity(req);
|
||||||
|
reset();
|
||||||
|
fetchData();
|
||||||
|
setNeedOtp(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: { id: TabKey; label: string }[] = [
|
const tabs: { id: TabKey; label: string }[] = [
|
||||||
{ id: "trending", label: "Trending" },
|
{ id: "trending", label: "Trending" },
|
||||||
|
|
@ -66,6 +162,35 @@ export default function DetailContent() {
|
||||||
{ id: "latest", label: "Latest" },
|
{ id: "latest", label: "Latest" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStateAdver();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function initStateAdver() {
|
||||||
|
const req = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
isPublish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAdvertise(req);
|
||||||
|
const data: Advertise[] = res?.data?.data || [1];
|
||||||
|
|
||||||
|
const banner = data.find((ad) => ad.placement === "jumbotron");
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
setBannerAd(banner);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching advertisement:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTabArticles();
|
fetchTabArticles();
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
@ -80,7 +205,6 @@ export default function DetailContent() {
|
||||||
sortBy: "created_at",
|
sortBy: "created_at",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sesuaikan sortBy berdasarkan tab
|
|
||||||
if (activeTab === "trending") {
|
if (activeTab === "trending") {
|
||||||
req.sortBy = "view_count";
|
req.sortBy = "view_count";
|
||||||
} else if (activeTab === "comments") {
|
} else if (activeTab === "comments") {
|
||||||
|
|
@ -89,7 +213,6 @@ export default function DetailContent() {
|
||||||
req.sortBy = "created_at";
|
req.sortBy = "created_at";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hanya kirimkan search jika valid
|
|
||||||
if (search && search !== "-" && search !== "") {
|
if (search && search !== "-" && search !== "") {
|
||||||
req.search = search;
|
req.search = search;
|
||||||
}
|
}
|
||||||
|
|
@ -180,17 +303,49 @@ export default function DetailContent() {
|
||||||
initStateData();
|
initStateData();
|
||||||
}, [listCategory]);
|
}, [listCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}, [detailFiles]);
|
||||||
|
|
||||||
async function initStateData() {
|
async function initStateData() {
|
||||||
loading();
|
loading();
|
||||||
const res = await getArticleById(id);
|
try {
|
||||||
const data = res.data?.data;
|
// 1️⃣ Ambil artikel by slug
|
||||||
|
const res = await getArticleBySlug(slug);
|
||||||
|
const data = res?.data?.data;
|
||||||
|
|
||||||
setThumbnail(data?.thumbnailUrl);
|
if (!data?.id) return;
|
||||||
setDiseId(data?.aiArticleId);
|
|
||||||
setDetailFiles(data?.files);
|
setArticleDetail(data);
|
||||||
setArticleDetail(data); // <-- Add this
|
setThumbnail(data.thumbnailUrl);
|
||||||
|
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 (
|
||||||
<>
|
<>
|
||||||
|
|
@ -232,39 +387,69 @@ export default function DetailContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-[#31942E] font-medium">
|
<span className="text-[#31942E] font-medium">
|
||||||
{articleDetail?.createdByName}
|
{articleDetail?.customCreatorName || articleDetail?.createdByName}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>
|
<span>
|
||||||
<span>
|
{new Date(articleDetail?.publishedAt ?? articleDetail?.createdAt)
|
||||||
{new Date(articleDetail?.createdAt).toLocaleDateString(
|
.toLocaleString("id-ID", {
|
||||||
"id-ID",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
</span>
|
hour12: false,
|
||||||
|
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">
|
||||||
{articleDetail?.files?.[0]?.file_url ? (
|
{/* Gambar utama */}
|
||||||
|
{detailFiles.length > 0 ? (
|
||||||
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={articleDetail.files[0].file_url}
|
src={detailFiles[selectedIndex]?.fileUrl}
|
||||||
alt="Berita"
|
alt={detailFiles[selectedIndex]?.fileAlt || "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="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
|
<div className="h-[400px] flex items-center justify-center bg-gray-100 rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p>
|
<p className="text-gray-400">Gambar tidak tersedia</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Slug */}
|
||||||
<p className="text-sm text-gray-500 mt-2 text-end">
|
<p className="text-sm text-gray-500 mt-2 text-end">
|
||||||
{articleDetail?.slug}
|
{articleDetail?.slug}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -336,23 +521,75 @@ export default function DetailContent() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<p className="text-gray-700 leading-relaxed text-justify">
|
<div className="prose max-w-none text-justify">
|
||||||
<span className="text-black font-bold text-md">
|
<div
|
||||||
Mikulnews.com -
|
dangerouslySetInnerHTML={removeImgTags(
|
||||||
</span>
|
formatTextToHtmlTag(articleDetail?.htmlDescription),
|
||||||
|
)}
|
||||||
|
className="text-sm lg:text-xl lg:leading-8 text-justify space-y-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{articleDetail?.description}
|
{/* <Author /> */}
|
||||||
</p>
|
<div className="w-full bg-white py-6">
|
||||||
<Author />
|
<p className="mx-10 text-2xl mb-4 ">AUTHOR</p>
|
||||||
|
<div className=" border border-black p-6 flex items-center gap-6 max-w-[1200px] mx-auto">
|
||||||
|
{/* Foto Profil */}
|
||||||
|
<div className="w-20 h-20 relative ">
|
||||||
|
<Image
|
||||||
|
src="/profile.jpg"
|
||||||
|
alt="Author"
|
||||||
|
fill
|
||||||
|
className="rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Author */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
{articleDetail?.customCreatorName ||
|
||||||
|
articleDetail?.createdByName}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
||||||
|
{/* Button lihat semua post */}
|
||||||
|
<button className="text-sm font-medium text-white hover:underline bg-[#655997] py-1 px-5 rounded-xl">
|
||||||
|
Lihat Semua Pos
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="bg-[#655997] rounded-full p-1">
|
||||||
|
<MailIcon
|
||||||
|
size={18}
|
||||||
|
className="text-white hover:text-black cursor-pointer "
|
||||||
|
></MailIcon>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#655997] rounded-full p-1">
|
||||||
|
<Link2
|
||||||
|
size={18}
|
||||||
|
className="text-white hover:text-black cursor-pointer "
|
||||||
|
></Link2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<span className="font-semibold text-sm text-gray-700">
|
<span className="font-semibold text-sm text-gray-700">
|
||||||
Tags:
|
Tags:
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
{articleDetail?.tags ? (
|
{articleDetail?.tags ? (
|
||||||
<span className="bg-gray-100 text-gray-700 text-sm px-2 py-1 rounded">
|
articleDetail.tags
|
||||||
{articleDetail.tags}
|
.split(",") // pisahkan berdasarkan koma
|
||||||
</span>
|
.map((tag: string, index: number) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
{tag.trim()}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500">Tidak ada tag</span>
|
<span className="text-sm text-gray-500">Tidak ada tag</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -360,16 +597,16 @@ export default function DetailContent() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mb-2 h-[120px] overflow-hidden flex items-center border my-8">
|
{/* <div className="relative mb-2 h-[120px] overflow-hidden flex items-center border my-8">
|
||||||
<Image
|
<Image
|
||||||
src={"/image-kolom.png"}
|
src={"/image-kolom.png"}
|
||||||
alt="Berita Utama"
|
alt="Berita Utama"
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<div className="flex items-center space-x-4 p-4 border rounded-lg mb-6">
|
{/* <div className="flex items-center space-x-4 p-4 border rounded-lg mb-6">
|
||||||
<Image
|
<Image
|
||||||
src={"/author.png"}
|
src={"/author.png"}
|
||||||
alt="Author"
|
alt="Author"
|
||||||
|
|
@ -382,15 +619,41 @@ export default function DetailContent() {
|
||||||
christine natalia
|
christine natalia
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-2">Tinggalkan Balasan</h2>
|
<h2 className="text-2xl font-bold mb-2">Tinggalkan Balasan</h2>
|
||||||
<p className="text-gray-600 mb-4 text-sm">
|
<p className="text-gray-600 mb-4 text-sm">
|
||||||
Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib
|
Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib
|
||||||
ditandai <span className="text-green-600">*</span>
|
ditandai <span className="text-green-600">*</span>
|
||||||
</p>
|
</p>
|
||||||
|
<div>
|
||||||
<form className="space-y-6 mt-6">
|
<h3 className="text-lg font-semibold border-b pb-2">Komentar</h3>
|
||||||
|
{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"
|
||||||
|
|
@ -401,10 +664,11 @@ 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"
|
||||||
required
|
{...register("comment", { required: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Nama */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="nama"
|
htmlFor="nama"
|
||||||
|
|
@ -416,11 +680,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"
|
||||||
required
|
{...register("name", { required: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* Email */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
|
|
@ -432,43 +696,51 @@ 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"
|
||||||
required
|
{...register("email", { required: true })}
|
||||||
/>
|
/>
|
||||||
</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"
|
className="bg-green-600 hover:bg-green-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2 w-full"
|
||||||
>
|
>
|
||||||
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>
|
||||||
|
|
@ -476,13 +748,32 @@ export default function DetailContent() {
|
||||||
<div className="md:col-span-1 space-y-6">
|
<div className="md:col-span-1 space-y-6">
|
||||||
<div className="sticky top-0 space-y-6">
|
<div className="sticky top-0 space-y-6">
|
||||||
<div className="bg-white shadow p-4 rounded-lg">
|
<div className="bg-white shadow p-4 rounded-lg">
|
||||||
|
{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={"/kolom.png"}
|
src={bannerAd.contentFileUrl}
|
||||||
alt="Iklan"
|
alt={bannerAd.title || "Iklan Banner"}
|
||||||
width={345}
|
width={1200} // ukuran dasar untuk responsive
|
||||||
height={345}
|
height={350}
|
||||||
className="rounded"
|
className="object-cover w-full h-full"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src="/kolom.png"
|
||||||
|
alt="Berita Utama"
|
||||||
|
width={1200}
|
||||||
|
height={188}
|
||||||
|
className="object-contain w-full h-[188px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button className="mt-4 w-full bg-black text-white py-2 rounded hover:opacity-90">
|
<button className="mt-4 w-full bg-black text-white py-2 rounded hover:opacity-90">
|
||||||
Learn More
|
Learn More
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// components/custom-editor.js
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
|
|
||||||
|
import "@/styles/custom-editor.css";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function CustomEditor(props) {
|
function CustomEditor(props) {
|
||||||
|
|
@ -47,7 +47,7 @@ function CustomEditor(props) {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0 !important;
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 1em 0 0.5em 0;
|
margin: 1em 0 0.5em 0;
|
||||||
|
|
@ -72,98 +72,6 @@ function CustomEditor(props) {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<style jsx>{`
|
|
||||||
.ckeditor-wrapper {
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
|
||||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-wrapper :global(.ck.ck-editor__main) {
|
|
||||||
min-height: ${props.height || 400}px;
|
|
||||||
max-height: ${maxHeight}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
|
||||||
min-height: ${(props.height || 400) - 50}px;
|
|
||||||
max-height: ${maxHeight - 50}px;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
|
||||||
background: #fff !important;
|
|
||||||
color: #111 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode support */
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
|
||||||
background: #111 !important;
|
|
||||||
color: #f9fafb !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h1),
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h2),
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h3),
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h4),
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h5),
|
|
||||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h6) {
|
|
||||||
color: #f9fafb !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable blockquote) {
|
|
||||||
background-color: #1f2937 !important;
|
|
||||||
border-left-color: #374151 !important;
|
|
||||||
color: #f3f4f6 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar styling for webkit browsers */
|
|
||||||
.ckeditor-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
|
||||||
background: #cbd5e1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode scrollbar */
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
|
||||||
background: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
|
||||||
background: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.ckeditor-wrapper
|
|
||||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure content doesn't overflow */
|
|
||||||
.ckeditor-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Editor } from '@tinymce/tinymce-react';
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
interface OptimizedEditorProps {
|
interface OptimizedEditorProps {
|
||||||
initialData?: string;
|
initialData?: string;
|
||||||
|
|
@ -9,14 +9,14 @@ interface OptimizedEditorProps {
|
||||||
height?: number;
|
height?: number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
||||||
initialData = '',
|
initialData = "",
|
||||||
onChange,
|
onChange,
|
||||||
height = 400,
|
height = 400,
|
||||||
placeholder = 'Start typing...',
|
placeholder = "Start typing...",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -42,14 +42,30 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
||||||
height,
|
height,
|
||||||
menubar: false,
|
menubar: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
"advlist",
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
"autolink",
|
||||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
"lists",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"code",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
],
|
],
|
||||||
toolbar: 'undo redo | blocks | ' +
|
toolbar:
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
"undo redo | blocks | " +
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
"bold italic forecolor | alignleft aligncenter " +
|
||||||
'removeformat | table | code | help',
|
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||||
|
"removeformat | table | code | help",
|
||||||
content_style: `
|
content_style: `
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
|
@ -69,18 +85,18 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
||||||
resize: false,
|
resize: false,
|
||||||
statusbar: false,
|
statusbar: false,
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
cache_suffix: '?v=1.0',
|
cache_suffix: "?v=1.0",
|
||||||
browser_spellcheck: false,
|
browser_spellcheck: false,
|
||||||
gecko_spellcheck: false,
|
gecko_spellcheck: false,
|
||||||
// Auto-save feature
|
// Auto-save feature
|
||||||
auto_save: true,
|
auto_save: true,
|
||||||
auto_save_interval: '30s',
|
auto_save_interval: "30s",
|
||||||
// Better mobile support
|
// Better mobile support
|
||||||
mobile: {
|
mobile: {
|
||||||
theme: 'silver',
|
theme: "silver",
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
toolbar: "bold italic | bullist numlist | link image",
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { Editor } from '@tinymce/tinymce-react';
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
interface TinyMCEEditorProps {
|
interface TinyMCEEditorProps {
|
||||||
initialData?: string;
|
initialData?: string;
|
||||||
|
|
@ -11,7 +11,7 @@ interface TinyMCEEditorProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
features?: 'basic' | 'standard' | 'full';
|
features?: "basic" | "standard" | "full";
|
||||||
toolbar?: string;
|
toolbar?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
uploadUrl?: string;
|
uploadUrl?: string;
|
||||||
|
|
@ -22,21 +22,21 @@ interface TinyMCEEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
initialData = '',
|
initialData = "",
|
||||||
onChange,
|
onChange,
|
||||||
onReady,
|
onReady,
|
||||||
height = 400,
|
height = 400,
|
||||||
placeholder = 'Start typing...',
|
placeholder = "Start typing...",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
features = 'standard',
|
features = "standard",
|
||||||
toolbar,
|
toolbar,
|
||||||
language = 'en',
|
language = "en",
|
||||||
uploadUrl,
|
uploadUrl,
|
||||||
uploadHeaders,
|
uploadHeaders,
|
||||||
className = '',
|
className = "",
|
||||||
autoSave = true,
|
autoSave = true,
|
||||||
autoSaveInterval = 30000
|
autoSaveInterval = 30000,
|
||||||
}) => {
|
}) => {
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
||||||
|
|
@ -47,35 +47,74 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
const getFeatureConfig = (featureLevel: string) => {
|
const getFeatureConfig = (featureLevel: string) => {
|
||||||
const configs = {
|
const configs = {
|
||||||
basic: {
|
basic: {
|
||||||
plugins: ['lists', 'link', 'autolink', 'wordcount'],
|
plugins: ["lists", "link", "autolink", "wordcount"],
|
||||||
toolbar: 'bold italic | bullist numlist | link',
|
toolbar: "bold italic | bullist numlist | link",
|
||||||
menubar: false
|
menubar: false,
|
||||||
},
|
},
|
||||||
standard: {
|
standard: {
|
||||||
plugins: [
|
plugins: [
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
"advlist",
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
"autolink",
|
||||||
'insertdatetime', 'media', 'table', 'help', 'wordcount'
|
"lists",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
],
|
],
|
||||||
toolbar: 'undo redo | blocks | ' +
|
toolbar:
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
"undo redo | blocks | " +
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
"bold italic forecolor | alignleft aligncenter " +
|
||||||
'removeformat | table | code | help',
|
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||||
menubar: false
|
"removeformat | table | code | help",
|
||||||
|
menubar: false,
|
||||||
},
|
},
|
||||||
full: {
|
full: {
|
||||||
plugins: [
|
plugins: [
|
||||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
"advlist",
|
||||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
"autolink",
|
||||||
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons',
|
"lists",
|
||||||
'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking',
|
"link",
|
||||||
'toc', 'imagetools', 'textpattern', 'codesample'
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
|
"emoticons",
|
||||||
|
"paste",
|
||||||
|
"textcolor",
|
||||||
|
"colorpicker",
|
||||||
|
"hr",
|
||||||
|
"pagebreak",
|
||||||
|
"nonbreaking",
|
||||||
|
"toc",
|
||||||
|
"imagetools",
|
||||||
|
"textpattern",
|
||||||
|
"codesample",
|
||||||
],
|
],
|
||||||
toolbar: 'undo redo | formatselect | bold italic backcolor | ' +
|
toolbar:
|
||||||
'alignleft aligncenter alignright alignjustify | ' +
|
"undo redo | formatselect | bold italic backcolor | " +
|
||||||
'bullist numlist outdent indent | removeformat | help',
|
"alignleft aligncenter alignright alignjustify | " +
|
||||||
menubar: 'file edit view insert format tools table help'
|
"bullist numlist outdent indent | removeformat | help",
|
||||||
}
|
menubar: "file edit view insert format tools table help",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return configs[featureLevel as keyof typeof configs] || configs.standard;
|
return configs[featureLevel as keyof typeof configs] || configs.standard;
|
||||||
};
|
};
|
||||||
|
|
@ -95,7 +134,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up word count tracking
|
// Set up word count tracking
|
||||||
editor.on('keyup', () => {
|
editor.on("keyup", () => {
|
||||||
const count = editor.plugins.wordcount.body.getCharacterCount();
|
const count = editor.plugins.wordcount.body.getCharacterCount();
|
||||||
setWordCount(count);
|
setWordCount(count);
|
||||||
});
|
});
|
||||||
|
|
@ -104,24 +143,24 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
if (autoSave && !readOnly) {
|
if (autoSave && !readOnly) {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const content = editor.getContent();
|
const content = editor.getContent();
|
||||||
localStorage.setItem('tinymce-autosave', content);
|
localStorage.setItem("tinymce-autosave", content);
|
||||||
setLastSaved(new Date());
|
setLastSaved(new Date());
|
||||||
}, autoSaveInterval);
|
}, autoSaveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix cursor jumping issues
|
// Fix cursor jumping issues
|
||||||
editor.on('keyup', (e: any) => {
|
editor.on("keyup", (e: any) => {
|
||||||
// Prevent cursor jumping on content changes
|
// Prevent cursor jumping on content changes
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.on('input', (e: any) => {
|
editor.on("input", (e: any) => {
|
||||||
// Prevent unnecessary re-renders
|
// Prevent unnecessary re-renders
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle paste events properly
|
// Handle paste events properly
|
||||||
editor.on('paste', (e: any) => {
|
editor.on("paste", (e: any) => {
|
||||||
// Allow default paste behavior
|
// Allow default paste behavior
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -130,23 +169,23 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
const handleImageUpload = (blobInfo: any, progress: any) => {
|
const handleImageUpload = (blobInfo: any, progress: any) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!uploadUrl) {
|
if (!uploadUrl) {
|
||||||
reject('No upload URL configured');
|
reject("No upload URL configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
formData.append("file", blobInfo.blob(), blobInfo.filename());
|
||||||
|
|
||||||
fetch(uploadUrl, {
|
fetch(uploadUrl, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: uploadHeaders || {},
|
headers: uploadHeaders || {},
|
||||||
body: formData
|
body: formData,
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(result => {
|
.then((result) => {
|
||||||
resolve(result.url);
|
resolve(result.url);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -158,17 +197,14 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
height,
|
height,
|
||||||
language,
|
language,
|
||||||
placeholder,
|
placeholder,
|
||||||
readonly: readOnly,
|
|
||||||
disabled,
|
|
||||||
branding: false,
|
branding: false,
|
||||||
elementpath: false,
|
elementpath: false,
|
||||||
resize: false,
|
resize: false,
|
||||||
statusbar: !readOnly,
|
statusbar: !readOnly,
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
cache_suffix: '?v=1.0',
|
cache_suffix: "?v=1.0",
|
||||||
browser_spellcheck: false,
|
browser_spellcheck: false,
|
||||||
gecko_spellcheck: false,
|
gecko_spellcheck: false,
|
||||||
// Content styling
|
|
||||||
content_style: `
|
content_style: `
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
|
@ -185,43 +221,41 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
// Image upload configuration
|
|
||||||
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
|
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
|
||||||
automatic_uploads: !!uploadUrl,
|
automatic_uploads: !!uploadUrl,
|
||||||
file_picker_types: 'image',
|
file_picker_types: "image",
|
||||||
// Better mobile support
|
|
||||||
mobile: {
|
mobile: {
|
||||||
theme: 'silver',
|
theme: "silver",
|
||||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||||
toolbar: 'bold italic | bullist numlist | link image'
|
toolbar: "bold italic | bullist numlist | link image",
|
||||||
},
|
},
|
||||||
// Paste configuration
|
|
||||||
paste_as_text: false,
|
paste_as_text: false,
|
||||||
paste_enable_default_filters: true,
|
paste_enable_default_filters: true,
|
||||||
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6",
|
||||||
paste_retain_style_properties: 'color background-color font-size font-weight',
|
paste_retain_style_properties:
|
||||||
// Table configuration
|
"color background-color font-size font-weight",
|
||||||
table_default_styles: {
|
table_default_styles: { width: "100%" },
|
||||||
width: '100%'
|
table_default_attributes: { border: "1" },
|
||||||
},
|
|
||||||
table_default_attributes: {
|
|
||||||
border: '1'
|
|
||||||
},
|
|
||||||
// Code configuration
|
|
||||||
codesample_languages: [
|
codesample_languages: [
|
||||||
{ text: 'HTML/XML', value: 'markup' },
|
{ text: "HTML/XML", value: "markup" },
|
||||||
{ text: 'JavaScript', value: 'javascript' },
|
{ text: "JavaScript", value: "javascript" },
|
||||||
{ text: 'CSS', value: 'css' },
|
{ text: "CSS", value: "css" },
|
||||||
{ text: 'PHP', value: 'php' },
|
{ text: "PHP", value: "php" },
|
||||||
{ text: 'Python', value: 'python' },
|
{ text: "Python", value: "python" },
|
||||||
{ text: 'Java', value: 'java' },
|
{ text: "Java", value: "java" },
|
||||||
{ text: 'C', value: 'c' },
|
{ text: "C", value: "c" },
|
||||||
{ text: 'C++', value: 'cpp' }
|
{ text: "C++", value: "cpp" },
|
||||||
],
|
],
|
||||||
// ...feature config
|
|
||||||
...featureConfig,
|
...featureConfig,
|
||||||
// Custom toolbar if provided
|
...(toolbar && { toolbar }),
|
||||||
...(toolbar && { toolbar })
|
setup: (editor: any) => {
|
||||||
|
// ⬅️ Set readOnly di sini
|
||||||
|
editor.on("init", () => {
|
||||||
|
if (readOnly) {
|
||||||
|
editor.mode.set("readonly");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -240,7 +274,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span>
|
<span>
|
||||||
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'}
|
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
|
||||||
</span>
|
</span>
|
||||||
{lastSaved && autoSave && !readOnly && (
|
{lastSaved && autoSave && !readOnly && (
|
||||||
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
||||||
|
|
@ -255,7 +289,12 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
|
|
||||||
{/* Performance indicator */}
|
{/* Performance indicator */}
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'}
|
Bundle size:{" "}
|
||||||
|
{features === "basic"
|
||||||
|
? "~150KB"
|
||||||
|
: features === "standard"
|
||||||
|
? "~200KB"
|
||||||
|
: "~300KB"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ function ViewEditor(props) {
|
||||||
.ckeditor-view-wrapper {
|
.ckeditor-view-wrapper {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
box-shadow:
|
||||||
|
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { CloudUploadIcon, TimesIcon } from "@/components/icons";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import ReactSelect from "react-select";
|
import ReactSelect from "react-select";
|
||||||
import makeAnimated from "react-select/animated";
|
import makeAnimated from "react-select/animated";
|
||||||
import { htmlToString } from "@/utils/global";
|
import { convertDateFormatNoTime, htmlToString } from "@/utils/global";
|
||||||
import { close, error, loading, successToast } from "@/config/swal";
|
import { close, error, loading, successToast } from "@/config/swal";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -44,6 +44,13 @@ import GenerateSingleArticleForm from "./generate-ai-single-form";
|
||||||
import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form";
|
import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import DatePicker from "react-datepicker";
|
||||||
|
|
||||||
const CustomEditor = dynamic(
|
const CustomEditor = dynamic(
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -82,6 +89,9 @@ const createArticleSchema = z.object({
|
||||||
title: z.string().min(2, {
|
title: z.string().min(2, {
|
||||||
message: "Judul harus diisi",
|
message: "Judul harus diisi",
|
||||||
}),
|
}),
|
||||||
|
customCreatorName: z.string().min(2, {
|
||||||
|
message: "Judul harus diisi",
|
||||||
|
}),
|
||||||
slug: z.string().min(2, {
|
slug: z.string().min(2, {
|
||||||
message: "Slug harus diisi",
|
message: "Slug harus diisi",
|
||||||
}),
|
}),
|
||||||
|
|
@ -94,6 +104,7 @@ const createArticleSchema = z.object({
|
||||||
tags: z.array(z.string()).nonempty({
|
tags: z.array(z.string()).nonempty({
|
||||||
message: "Minimal 1 tag",
|
message: "Minimal 1 tag",
|
||||||
}),
|
}),
|
||||||
|
source: z.enum(["internal", "external"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function CreateArticleForm() {
|
export default function CreateArticleForm() {
|
||||||
|
|
@ -117,8 +128,8 @@ export default function CreateArticleForm() {
|
||||||
"publish"
|
"publish"
|
||||||
);
|
);
|
||||||
const [isScheduled, setIsScheduled] = useState(false);
|
const [isScheduled, setIsScheduled] = useState(false);
|
||||||
|
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
|
||||||
const [startDateValue, setStartDateValue] = useState<any>(null);
|
const [startTimeValue, setStartTimeValue] = useState<string>("");
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
onDrop: (acceptedFiles) => {
|
onDrop: (acceptedFiles) => {
|
||||||
|
|
@ -225,6 +236,8 @@ export default function CreateArticleForm() {
|
||||||
const request = {
|
const request = {
|
||||||
id: diseData?.id,
|
id: diseData?.id,
|
||||||
title: values.title,
|
title: values.title,
|
||||||
|
customCreatorName: values.customCreatorName,
|
||||||
|
source: values.source,
|
||||||
articleBody: removeImgTags(values.description),
|
articleBody: removeImgTags(values.description),
|
||||||
metaDescription: diseData?.metaDescription,
|
metaDescription: diseData?.metaDescription,
|
||||||
metaTitle: diseData?.metaTitle,
|
metaTitle: diseData?.metaTitle,
|
||||||
|
|
@ -280,6 +293,8 @@ export default function CreateArticleForm() {
|
||||||
title: values.title,
|
title: values.title,
|
||||||
typeId: 1,
|
typeId: 1,
|
||||||
slug: values.slug,
|
slug: values.slug,
|
||||||
|
customCreatorName: values.customCreatorName,
|
||||||
|
source: values.source,
|
||||||
categoryIds: values.category.map((a) => a.id).join(","),
|
categoryIds: values.category.map((a) => a.id).join(","),
|
||||||
tags: values.tags.join(","),
|
tags: values.tags.join(","),
|
||||||
description: htmlToString(removeImgTags(values.description)),
|
description: htmlToString(removeImgTags(values.description)),
|
||||||
|
|
@ -324,12 +339,34 @@ export default function CreateArticleForm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "scheduled") {
|
if (status === "scheduled" && startDateValue) {
|
||||||
|
// ambil waktu, default 00:00 jika belum diisi
|
||||||
|
const [hours, minutes] = startTimeValue
|
||||||
|
? startTimeValue.split(":").map(Number)
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
// gabungkan tanggal + waktu
|
||||||
|
const combinedDate = new Date(startDateValue);
|
||||||
|
combinedDate.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
|
// format: 2025-10-08 14:30:00
|
||||||
|
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
|
||||||
|
combinedDate.getMonth() + 1
|
||||||
|
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
|
||||||
|
combinedDate.getMinutes()
|
||||||
|
).padStart(2, "0")}:00`;
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
id: articleId,
|
id: articleId,
|
||||||
date: `${startDateValue?.year}-${startDateValue?.month}-${startDateValue?.day}`,
|
date: formattedDateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("📤 Sending schedule request:", request);
|
||||||
const res = await createArticleSchedule(request);
|
const res = await createArticleSchedule(request);
|
||||||
|
console.log("✅ Schedule response:", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
close();
|
close();
|
||||||
|
|
@ -524,13 +561,21 @@ export default function CreateArticleForm() {
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="single">Single Article</SelectItem>
|
<SelectItem value="single">Single Article</SelectItem>
|
||||||
<SelectItem value="rewrite">Content Rewrite</SelectItem>
|
{/* <SelectItem value="rewrite">Content Rewrite</SelectItem> */}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{selectedWritingType === "single" ? (
|
{selectedWritingType === "single" ? (
|
||||||
<GenerateSingleArticleForm
|
<GenerateSingleArticleForm
|
||||||
content={(data) => {
|
content={(data) => {
|
||||||
setDiseData(data);
|
setDiseData(data);
|
||||||
|
// setValue("title", data?.title ?? "", {
|
||||||
|
// shouldValidate: true,
|
||||||
|
// shouldDirty: true,
|
||||||
|
// });
|
||||||
|
// setValue("slug", generateSlug(data?.title ?? ""), {
|
||||||
|
// shouldValidate: true,
|
||||||
|
// shouldDirty: true,
|
||||||
|
// });
|
||||||
setValue(
|
setValue(
|
||||||
"description",
|
"description",
|
||||||
data?.articleBody ? data?.articleBody : ""
|
data?.articleBody ? data?.articleBody : ""
|
||||||
|
|
@ -651,7 +696,38 @@ export default function CreateArticleForm() {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<p className="text-sm">Kreator</p>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="customCreatorName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
id="customCreatorName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan judul artikel"
|
||||||
|
className="w-full border rounded-lg dark:border-gray-400"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm">Tipe Kreator</p>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="source"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger className="w-full border rounded-lg text-sm dark:border-gray-400">
|
||||||
|
<SelectValue placeholder="Pilih tipe kreator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="internal">Internal</SelectItem>
|
||||||
|
<SelectItem value="external">External</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p className="text-sm mt-3">Kategori</p>
|
<p className="text-sm mt-3">Kategori</p>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -765,32 +841,49 @@ export default function CreateArticleForm() {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* {isScheduled && (
|
{isScheduled && (
|
||||||
<div className="flex flex-col lg:flex-row gap-3">
|
<div className="flex flex-col lg:flex-row gap-3 mt-2">
|
||||||
<div className="w-full lg:w-[140px] flex flex-col gal-2 ">
|
{/* Pilih tanggal */}
|
||||||
|
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||||
<p className="text-sm">Tanggal</p>
|
<p className="text-sm">Tanggal</p>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full !h-[30px] lg:h-[40px] border-1 rounded-lg text-black"
|
className="w-full !h-[37px] lg:h-[37px] border-1 rounded-lg text-black"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{startDateValue
|
{startDateValue
|
||||||
? convertDateFormatNoTime(startDateValue)
|
? startDateValue.toLocaleDateString("en-CA")
|
||||||
: "-"}
|
: "-"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="bg-transparent p-0">
|
<PopoverContent className="bg-transparent p-0">
|
||||||
<Calendar
|
<DatePicker
|
||||||
selected={startDateValue}
|
selected={startDateValue}
|
||||||
onSelect={setStartDateValue}
|
onChange={(date) =>
|
||||||
|
setStartDateValue(date ?? undefined)
|
||||||
|
}
|
||||||
|
dateFormat="yyyy-MM-dd"
|
||||||
|
className="w-full border rounded-lg px-2 py-1 text-black cursor-pointer h-[150px]"
|
||||||
|
placeholderText="Pilih tanggal"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pilih waktu */}
|
||||||
|
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||||
|
<p className="text-sm">Waktu</p>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={startTimeValue}
|
||||||
|
onChange={(e) => setStartTimeValue(e.target.value)}
|
||||||
|
className="w-full border rounded-lg px-2 py-[6px] text-black"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,13 @@ import GetSeoScore from "./get-seo-score-form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import {
|
import {
|
||||||
|
createArticleSchedule,
|
||||||
deleteArticleFiles,
|
deleteArticleFiles,
|
||||||
getArticleByCategory,
|
getArticleByCategory,
|
||||||
getArticleById,
|
getArticleById,
|
||||||
|
getArticleFiles,
|
||||||
submitApproval,
|
submitApproval,
|
||||||
|
unPublishArticle,
|
||||||
updateArticle,
|
updateArticle,
|
||||||
uploadArticleFile,
|
uploadArticleFile,
|
||||||
uploadArticleThumbnail,
|
uploadArticleThumbnail,
|
||||||
|
|
@ -48,6 +51,15 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import DatePicker from "react-datepicker";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
const ViewEditor = dynamic(
|
const ViewEditor = dynamic(
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -81,6 +93,9 @@ const createArticleSchema = z.object({
|
||||||
title: z.string().min(2, {
|
title: z.string().min(2, {
|
||||||
message: "Judul harus diisi",
|
message: "Judul harus diisi",
|
||||||
}),
|
}),
|
||||||
|
customCreatorName: z.string().min(2, {
|
||||||
|
message: "Judul harus diisi",
|
||||||
|
}),
|
||||||
slug: z.string().min(2, {
|
slug: z.string().min(2, {
|
||||||
message: "Slug harus diisi",
|
message: "Slug harus diisi",
|
||||||
}),
|
}),
|
||||||
|
|
@ -92,7 +107,8 @@ const createArticleSchema = z.object({
|
||||||
}),
|
}),
|
||||||
tags: z.array(z.string()).nonempty({
|
tags: z.array(z.string()).nonempty({
|
||||||
message: "Minimal 1 tag",
|
message: "Minimal 1 tag",
|
||||||
}), // Array berisi string
|
}),
|
||||||
|
source: z.enum(["internal", "external"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface DiseData {
|
interface DiseData {
|
||||||
|
|
@ -136,8 +152,14 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
const [approvalStatus, setApprovalStatus] = useState<number>(2);
|
const [approvalStatus, setApprovalStatus] = useState<number>(2);
|
||||||
const [approvalMessage, setApprovalMessage] = useState("");
|
const [approvalMessage, setApprovalMessage] = useState("");
|
||||||
const [detailData, setDetailData] = useState<any>();
|
const [detailData, setDetailData] = useState<any>();
|
||||||
const [startDateValue, setStartDateValue] = useState<any>(null);
|
// const [startDateValue, setStartDateValue] = useState<any>(null);
|
||||||
const [timeValue, setTimeValue] = useState("00:00");
|
// const [timeValue, setTimeValue] = useState("00:00");
|
||||||
|
const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
|
||||||
|
"publish"
|
||||||
|
);
|
||||||
|
const [isScheduled, setIsScheduled] = useState(false);
|
||||||
|
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
|
||||||
|
const [startTimeValue, setStartTimeValue] = useState<string>("");
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
onDrop: (acceptedFiles) => {
|
onDrop: (acceptedFiles) => {
|
||||||
|
|
@ -175,20 +197,50 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
async function initState() {
|
async function initState() {
|
||||||
loading();
|
loading();
|
||||||
const res = await getArticleById(id);
|
try {
|
||||||
const data = res.data?.data;
|
// 1️⃣ Ambil ARTICLE
|
||||||
setDetailData(data);
|
const articleRes = await getArticleById(id);
|
||||||
setValue("title", data?.title);
|
const articleData = articleRes.data?.data;
|
||||||
setValue("slug", data?.slug);
|
|
||||||
setValue("description", data?.htmlDescription);
|
|
||||||
setValue("tags", data?.tags ? data.tags.split(",") : []);
|
|
||||||
setThumbnail(data?.thumbnailUrl);
|
|
||||||
setDiseId(data?.aiArticleId);
|
|
||||||
setDetailFiles(data?.files);
|
|
||||||
|
|
||||||
setupInitCategory(data?.categories);
|
if (!articleData) return;
|
||||||
|
|
||||||
|
// ===== 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[] = [];
|
||||||
|
|
@ -242,21 +294,28 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
const doPublish = async () => {
|
const doPublish = async () => {
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
title: "Publish Artikel?",
|
title: isScheduled ? "Jadwalkan Publikasi?" : "Publish Artikel Sekarang?",
|
||||||
text: "",
|
text: isScheduled
|
||||||
|
? "Artikel akan dipublish otomatis sesuai tanggal dan waktu yang kamu pilih."
|
||||||
|
: "",
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
cancelButtonColor: "#d33",
|
cancelButtonColor: "#d33",
|
||||||
confirmButtonColor: "#3085d6",
|
confirmButtonColor: "#3085d6",
|
||||||
confirmButtonText: "Submit",
|
confirmButtonText: isScheduled ? "Jadwalkan" : "Publish",
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
publish();
|
if (isScheduled) {
|
||||||
|
setStatus("scheduled");
|
||||||
|
publishScheduled();
|
||||||
|
} else {
|
||||||
|
publishNow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const publish = async () => {
|
const publishNow = async () => {
|
||||||
const response = await updateArticle(String(id), {
|
const response = await updateArticle(String(id), {
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
isPublish: true,
|
isPublish: true,
|
||||||
|
|
@ -273,8 +332,67 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
error(response.message);
|
error(response.message);
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
successSubmit("/admin/article");
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishScheduled = async () => {
|
||||||
|
if (!startDateValue) {
|
||||||
|
error("Tanggal belum dipilih!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hours, minutes] = startTimeValue
|
||||||
|
? startTimeValue.split(":").map(Number)
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
const combinedDate = new Date(startDateValue);
|
||||||
|
combinedDate.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
|
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
|
||||||
|
combinedDate.getMonth() + 1
|
||||||
|
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
|
||||||
|
combinedDate.getMinutes()
|
||||||
|
).padStart(2, "0")}:00`;
|
||||||
|
|
||||||
|
const response = await updateArticle(String(id), {
|
||||||
|
id: Number(id),
|
||||||
|
isPublish: false,
|
||||||
|
title: detailData?.title,
|
||||||
|
typeId: 1,
|
||||||
|
slug: detailData?.slug,
|
||||||
|
categoryIds: getValues("category")
|
||||||
|
.map((val) => val.id)
|
||||||
|
.join(","),
|
||||||
|
tags: getValues("tags").join(","),
|
||||||
|
description: htmlToString(getValues("description")),
|
||||||
|
htmlDescription: getValues("description"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleId = response?.data?.data?.id ?? id;
|
||||||
|
const scheduleReq = {
|
||||||
|
id: articleId,
|
||||||
|
date: formattedDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📅 Mengirim jadwal publish:", scheduleReq);
|
||||||
|
const res = await createArticleSchedule(scheduleReq);
|
||||||
|
|
||||||
|
if (res?.error) {
|
||||||
|
error("Gagal membuat jadwal publikasi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
successSubmit("/admin/article");
|
successSubmit("/admin/article");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -292,16 +410,16 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
// createdAt: `${startDateValue} ${timeValue}:00`,
|
// createdAt: `${startDateValue} ${timeValue}:00`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (startDateValue && timeValue) {
|
// if (startDateValue && timeValue) {
|
||||||
formData.createdAt = `${startDateValue} ${timeValue}:00`;
|
// formData.createdAt = `${startDateValue} ${timeValue}:00`;
|
||||||
}
|
// }
|
||||||
const response = await updateArticle(String(id), formData);
|
const response = await updateArticle(String(id), formData);
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
error(response.message);
|
error(response.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const articleId = response?.data?.data?.id;
|
||||||
const formFiles = new FormData();
|
const formFiles = new FormData();
|
||||||
|
|
||||||
if (files?.length > 0) {
|
if (files?.length > 0) {
|
||||||
|
|
@ -318,8 +436,38 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
const resFile = await uploadArticleThumbnail(String(id), formFiles);
|
const resFile = await uploadArticleThumbnail(String(id), formFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === "scheduled" && startDateValue) {
|
||||||
|
// ambil waktu, default 00:00 jika belum diisi
|
||||||
|
const [hours, minutes] = startTimeValue
|
||||||
|
? startTimeValue.split(":").map(Number)
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
// gabungkan tanggal + waktu
|
||||||
|
const combinedDate = new Date(startDateValue);
|
||||||
|
combinedDate.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
|
// format: 2025-10-08 14:30:00
|
||||||
|
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
|
||||||
|
combinedDate.getMonth() + 1
|
||||||
|
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
|
||||||
|
combinedDate.getMinutes()
|
||||||
|
).padStart(2, "0")}:00`;
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
id: articleId,
|
||||||
|
date: formattedDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📤 Sending schedule request:", request);
|
||||||
|
const res = await createArticleSchedule(request);
|
||||||
|
console.log("✅ Schedule response:", res);
|
||||||
|
}
|
||||||
|
|
||||||
close();
|
close();
|
||||||
successSubmit("/admin/article");
|
successSubmitData();
|
||||||
};
|
};
|
||||||
|
|
||||||
function successSubmit(redirect: string) {
|
function successSubmit(redirect: string) {
|
||||||
|
|
@ -335,6 +483,51 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function successSubmitData() {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Berhasil disimpan!",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doUnpublish = async () => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Unpublish Artikel?",
|
||||||
|
text: "Artikel akan dihapus dari publik dan tidak tampil lagi.",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: "#d33",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "Ya, Unpublish",
|
||||||
|
}).then(async (result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
loading();
|
||||||
|
const response = await unPublishArticle(String(id), {
|
||||||
|
id: Number(id),
|
||||||
|
isPublish: false,
|
||||||
|
title: detailData?.title,
|
||||||
|
typeId: 1,
|
||||||
|
slug: detailData?.slug,
|
||||||
|
categoryIds: getValues("category")
|
||||||
|
.map((val) => val.id)
|
||||||
|
.join(","),
|
||||||
|
tags: getValues("tags").join(","),
|
||||||
|
description: htmlToString(getValues("description")),
|
||||||
|
htmlDescription: getValues("description"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
successSubmit("/admin/article");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const watchTitle = watch("title");
|
const watchTitle = watch("title");
|
||||||
const generateSlug = (title: string) => {
|
const generateSlug = (title: string) => {
|
||||||
return title
|
return title
|
||||||
|
|
@ -497,9 +690,10 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
name="title"
|
name="title"
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="title" className="block text-sm font-medium mb-1">
|
<label htmlFor="title" className="block text-xl font-medium mb-2">
|
||||||
Judul
|
Judul
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="title"
|
id="title"
|
||||||
|
|
@ -507,7 +701,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
readOnly={isDetail}
|
readOnly={isDetail}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="w-full border rounded-lg"
|
className="h-16 px-4 text-2xl leading-tight"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -623,9 +817,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
alt="main"
|
alt="main"
|
||||||
width={720}
|
width={720}
|
||||||
height={480}
|
height={480}
|
||||||
src={
|
src={detailfiles[mainImage]?.fileUrl || "/default-avatar.png"}
|
||||||
detailfiles[mainImage]?.file_url || "/default-avatar.png"
|
|
||||||
}
|
|
||||||
className="w-[75%] mx-auto"
|
className="w-[75%] mx-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -640,7 +832,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
width={480}
|
width={480}
|
||||||
height={360}
|
height={360}
|
||||||
alt={`image-${index}`}
|
alt={`image-${index}`}
|
||||||
src={file.file_url || "/default-avatar.png"}
|
src={file.fileUrl || "/default-avatar.png"}
|
||||||
className="h-[100px] object-cover w-[150px]"
|
className="h-[100px] object-cover w-[150px]"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -663,7 +855,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
width={480}
|
width={480}
|
||||||
height={360}
|
height={360}
|
||||||
alt={`image-${index}`}
|
alt={`image-${index}`}
|
||||||
src={file?.file_url || "/default-avatar.png"}
|
src={file?.fileUrl || "/default-avatar.png"}
|
||||||
className="h-[100px] object-cover w-[150px]"
|
className="h-[100px] object-cover w-[150px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -683,6 +875,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
className=" border-none rounded-full"
|
className=" border-none rounded-full"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="danger"
|
color="danger"
|
||||||
|
|
@ -784,6 +977,43 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<p className="text-sm">Kreator</p>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="customCreatorName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
id="customCreatorName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Kreator artikel"
|
||||||
|
readOnly={isDetail}
|
||||||
|
className="w-full border rounded-lg dark:border-gray-400"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm">Tipe Kreator</p>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="source"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
disabled={isDetail}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border rounded-lg text-sm dark:border-gray-400">
|
||||||
|
<SelectValue placeholder="Pilih tipe kreator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="internal">Internal</SelectItem>
|
||||||
|
<SelectItem value="external">External</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p className="text-sm mt-3">Kategori</p>
|
<p className="text-sm mt-3">Kategori</p>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -885,7 +1115,63 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
{errors?.tags && (
|
{errors?.tags && (
|
||||||
<p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p>
|
<p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p>
|
||||||
)}
|
)}
|
||||||
{!isDetail && username === "admin-mabes" && (
|
<div className="flex flex-col gap-2 mt-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="schedule-switch"
|
||||||
|
checked={isScheduled}
|
||||||
|
onCheckedChange={setIsScheduled}
|
||||||
|
/>
|
||||||
|
<label htmlFor="schedule-switch" className="text-black text-sm">
|
||||||
|
Publish dengan Jadwal
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isScheduled && (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-3 mt-2">
|
||||||
|
{/* Pilih tanggal */}
|
||||||
|
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||||
|
<p className="text-sm">Tanggal</p>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full !h-[37px] lg:h-[37px] border-1 rounded-lg text-black"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{startDateValue
|
||||||
|
? startDateValue.toLocaleDateString("en-CA")
|
||||||
|
: "-"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="bg-transparent p-0">
|
||||||
|
<DatePicker
|
||||||
|
selected={startDateValue}
|
||||||
|
onChange={(date) =>
|
||||||
|
setStartDateValue(date ?? undefined)
|
||||||
|
}
|
||||||
|
dateFormat="yyyy-MM-dd"
|
||||||
|
className="w-full border rounded-lg px-2 py-1 text-black cursor-pointer h-[150px]"
|
||||||
|
placeholderText="Pilih tanggal"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pilih waktu */}
|
||||||
|
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||||
|
<p className="text-sm">Waktu</p>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={startTimeValue}
|
||||||
|
onChange={(e) => setStartTimeValue(e.target.value)}
|
||||||
|
className="w-full border rounded-lg px-2 py-[6px] text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* {!isDetail && username === "admin-mabes" && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm">Ubah Waktu Pembuatan</p>
|
<p className="text-sm">Ubah Waktu Pembuatan</p>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
|
|
@ -917,7 +1203,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end gap-3">
|
<div className="flex flex-row justify-end gap-3">
|
||||||
|
|
@ -955,8 +1241,13 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{detailData?.isPublish === false && (
|
{detailData?.isPublish === false && (
|
||||||
<Button type="button" color="primary" onClick={doPublish}>
|
<Button
|
||||||
Publish
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
onClick={doPublish}
|
||||||
|
disabled={isScheduled && !startDateValue}
|
||||||
|
>
|
||||||
|
{isScheduled ? "Jadwalkan" : "Publish"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* {!isDetail && (
|
{/* {!isDetail && (
|
||||||
|
|
@ -965,6 +1256,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
</Button>
|
</Button>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
|
{detailData?.isPublish == true && (
|
||||||
|
<Button
|
||||||
|
className="bg-red-500 text-white"
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={doUnpublish}
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link href="/admin/article">
|
<Link href="/admin/article">
|
||||||
<Button color="danger" variant="outline" type="button">
|
<Button color="danger" variant="outline" type="button">
|
||||||
Kembali
|
Kembali
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { close, error, loading } from "@/config/swal";
|
import { close, error, loading } from "@/config/swal";
|
||||||
import { delay } from "@/utils/global";
|
import { delay } from "@/utils/global";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { getDetailArticle, getGenerateRewriter } from "@/service/generate-article";
|
import {
|
||||||
|
getDetailArticle,
|
||||||
|
getGenerateRewriter,
|
||||||
|
} from "@/service/generate-article";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import GetSeoScore from "./get-seo-score-form";
|
import GetSeoScore from "./get-seo-score-form";
|
||||||
|
|
@ -69,8 +78,11 @@ interface DiseData {
|
||||||
additionalKeywords: string;
|
additionalKeywords: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenerateContentRewriteForm(props: { content: (data: DiseData) => void }) {
|
export default function GenerateContentRewriteForm(props: {
|
||||||
const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational");
|
content: (data: DiseData) => void;
|
||||||
|
}) {
|
||||||
|
const [selectedWritingSyle, setSelectedWritingStyle] =
|
||||||
|
useState("Informational");
|
||||||
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
|
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState("id");
|
const [selectedLanguage, setSelectedLanguage] = useState("id");
|
||||||
const [mainKeyword, setMainKeyword] = useState("");
|
const [mainKeyword, setMainKeyword] = useState("");
|
||||||
|
|
@ -166,7 +178,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
))}
|
))}
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select value={selectedWritingSyle} onValueChange={(value) => setSelectedWritingStyle(value)}>
|
<Select
|
||||||
|
value={selectedWritingSyle}
|
||||||
|
onValueChange={(value) => setSelectedWritingStyle(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||||
<SelectValue placeholder="Writing Style" />
|
<SelectValue placeholder="Writing Style" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -198,7 +213,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
))}
|
))}
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select value={selectedArticleSize} onValueChange={(value) => setSelectedArticleSize(value)}>
|
<Select
|
||||||
|
value={selectedArticleSize}
|
||||||
|
onValueChange={(value) => setSelectedArticleSize(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||||
<SelectValue placeholder="Writing Style" />
|
<SelectValue placeholder="Writing Style" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -229,7 +247,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
<SelectItem key="en">English</SelectItem>
|
<SelectItem key="en">English</SelectItem>
|
||||||
</SelectSection>
|
</SelectSection>
|
||||||
</Select> */}
|
</Select> */}
|
||||||
<Select value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)}>
|
<Select
|
||||||
|
value={selectedLanguage}
|
||||||
|
onValueChange={(value) => setSelectedLanguage(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||||
<SelectValue placeholder="Writing Style" />
|
<SelectValue placeholder="Writing Style" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -239,6 +260,7 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col mt-3">
|
<div className="flex flex-col mt-3">
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<p className="text-sm">Text</p>
|
<p className="text-sm">Text</p>
|
||||||
|
|
@ -246,9 +268,16 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
<div className="w-[78vw] lg:w-full">
|
<div className="w-[78vw] lg:w-full">
|
||||||
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
|
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
|
||||||
</div>
|
</div>
|
||||||
{mainKeyword == "" && <p className="text-red-400 text-sm">Required</p>}
|
{mainKeyword == "" && (
|
||||||
|
<p className="text-red-400 text-sm">Required</p>
|
||||||
|
)}
|
||||||
{articleIds.length < 3 && (
|
{articleIds.length < 3 && (
|
||||||
<Button onClick={onSubmit} type="button" disabled={mainKeyword === "" || isLoading} className="my-5 w-full py-5 text-xs md:text-base">
|
<Button
|
||||||
|
onClick={onSubmit}
|
||||||
|
type="button"
|
||||||
|
disabled={mainKeyword === "" || isLoading}
|
||||||
|
className="my-5 w-full py-5 text-xs md:text-base"
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
|
@ -263,7 +292,14 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
|
||||||
{articleIds.length > 0 && (
|
{articleIds.length > 0 && (
|
||||||
<div className="flex flex-row gap-1 mt-2">
|
<div className="flex flex-row gap-1 mt-2">
|
||||||
{articleIds?.map((id, index) => (
|
{articleIds?.map((id, index) => (
|
||||||
<Button key={id} onClick={() => setSelectedId(id)} disabled={isLoading && selectedId === id} variant={selectedId === id ? "default" : "outline"} className="flex items-center gap-2">
|
<Button
|
||||||
|
type="button"
|
||||||
|
key={id}
|
||||||
|
onClick={() => setSelectedId(id)}
|
||||||
|
disabled={isLoading && selectedId === id}
|
||||||
|
variant={selectedId === id ? "default" : "outline"}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
{isLoading && selectedId === id ? (
|
{isLoading && selectedId === id ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
|
|
||||||
|
|
@ -76,16 +76,15 @@ interface DiseData {
|
||||||
export default function GenerateSingleArticleForm(props: {
|
export default function GenerateSingleArticleForm(props: {
|
||||||
content: (data: DiseData) => void;
|
content: (data: DiseData) => void;
|
||||||
}) {
|
}) {
|
||||||
const [selectedWritingSyle, setSelectedWritingStyle] =
|
const [selectedWritingSyle, setSelectedWritingStyle] = useState("");
|
||||||
useState("Informational");
|
const [selectedArticleSize, setSelectedArticleSize] = useState("");
|
||||||
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
|
const [selectedLanguage, setSelectedLanguage] = useState("");
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState("id");
|
|
||||||
const [mainKeyword, setMainKeyword] = useState("");
|
const [mainKeyword, setMainKeyword] = useState("");
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [additionalKeyword, setAdditionalKeyword] = useState("");
|
const [additionalKeyword, setAdditionalKeyword] = useState("");
|
||||||
const [articleIds, setArticleIds] = useState<number[]>([]);
|
const [articleIds, setArticleIds] = useState<number[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState<number>();
|
const [selectedId, setSelectedId] = useState<number>();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const generateAll = async (keyword: string | undefined) => {
|
const generateAll = async (keyword: string | undefined) => {
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
|
|
@ -271,11 +270,11 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
||||||
<SelectValue placeholder="Writing Style" />
|
<SelectValue placeholder="Article Size" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{articleSize.map((style) => (
|
{articleSize.map((style) => (
|
||||||
<SelectItem key={style.name} value={style.name}>
|
<SelectItem key={style.name} value={style.value}>
|
||||||
{style.name}
|
{style.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -307,7 +306,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
||||||
<SelectValue placeholder="Bahasa" />
|
<SelectValue placeholder="Language" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="id">Indonesia</SelectItem>
|
<SelectItem value="id">Indonesia</SelectItem>
|
||||||
|
|
@ -319,6 +318,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<p className="text-sm">Main Keyword</p>
|
<p className="text-sm">Main Keyword</p>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => generateAll(mainKeyword)}
|
onClick={() => generateAll(mainKeyword)}
|
||||||
|
|
@ -350,6 +350,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
<div className="flex flex-row gap-2 items-center mt-3">
|
<div className="flex flex-row gap-2 items-center mt-3">
|
||||||
<p className="text-sm">Title</p>
|
<p className="text-sm">Title</p>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => generateTitle(mainKeyword)}
|
onClick={() => generateTitle(mainKeyword)}
|
||||||
|
|
@ -373,6 +374,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
<div className="flex flex-row gap-2 items-center mt-2">
|
<div className="flex flex-row gap-2 items-center mt-2">
|
||||||
<p className="text-sm">Additional Keyword</p>
|
<p className="text-sm">Additional Keyword</p>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => generateKeywords(mainKeyword)}
|
onClick={() => generateKeywords(mainKeyword)}
|
||||||
|
|
@ -417,6 +419,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
<div className="flex flex-row gap-1 mt-2">
|
<div className="flex flex-row gap-1 mt-2">
|
||||||
{articleIds.map((id, index) => (
|
{articleIds.map((id, index) => (
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => setSelectedId(id)}
|
onClick={() => setSelectedId(id)}
|
||||||
disabled={isLoading && selectedId === id}
|
disabled={isLoading && selectedId === id}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import news from "../news";
|
import news from "../news";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getAdvertise } from "@/service/advertisement";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -12,13 +13,15 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -32,6 +35,15 @@ const slugToLabel = (slug: string) => {
|
||||||
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
|
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Advertise = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placement: string;
|
||||||
|
contentFileUrl: string;
|
||||||
|
redirectLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CitizenNews() {
|
export default function CitizenNews() {
|
||||||
const [activeTab, setActiveTab] = useState("comments");
|
const [activeTab, setActiveTab] = useState("comments");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
@ -51,6 +63,36 @@ export default function CitizenNews() {
|
||||||
const categorySlug = pathSegments[1];
|
const categorySlug = pathSegments[1];
|
||||||
const categoryLabel = slugToLabel(categorySlug);
|
const categoryLabel = slugToLabel(categorySlug);
|
||||||
|
|
||||||
|
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStateAdver();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function initStateAdver() {
|
||||||
|
const req = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
isPublish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAdvertise(req);
|
||||||
|
const data: Advertise[] = res?.data?.data || [1];
|
||||||
|
|
||||||
|
// filter iklan dengan placement = "banner"
|
||||||
|
const banner = data.find((ad) => ad.placement === "jumbotron");
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
setBannerAd(banner);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching advertisement:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initState();
|
initState();
|
||||||
}, [page, showData, startDateValue, selectedCategories, activeTab]);
|
}, [page, showData, startDateValue, selectedCategories, activeTab]);
|
||||||
|
|
@ -81,8 +123,8 @@ export default function CitizenNews() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const citizenArticles = articles.filter((article) =>
|
const citizenArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "berita warga"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -109,7 +151,7 @@ export default function CitizenNews() {
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<Link
|
<Link
|
||||||
className="flex flex-col md:flex-row gap-6"
|
className="flex flex-col md:flex-row gap-6"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
{/* Image + Category */}
|
{/* Image + Category */}
|
||||||
<div className="relative w-full md:w-1/2 h-64">
|
<div className="relative w-full md:w-1/2 h-64">
|
||||||
|
|
@ -132,7 +174,7 @@ export default function CitizenNews() {
|
||||||
<div className="text-sm text-gray-600 mt-2">
|
<div className="text-sm text-gray-600 mt-2">
|
||||||
BY{" "}
|
BY{" "}
|
||||||
<span className="text-green-600 font-semibold">
|
<span className="text-green-600 font-semibold">
|
||||||
{item.createdByName || "Admin"}
|
{item?.customCreatorName || item.createdByName}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -204,13 +246,33 @@ export default function CitizenNews() {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Advertisement */}
|
{/* Advertisement */}
|
||||||
<div className="w-full h-[400px] relative">
|
<div className="w-full h-[400px] relative">
|
||||||
|
{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="/advertisiment.png"
|
src={bannerAd.contentFileUrl}
|
||||||
alt="Advertisement"
|
alt={bannerAd.title || "Iklan Banner"}
|
||||||
fill
|
width={1200} // ukuran dasar untuk responsive
|
||||||
className="object-cover rounded"
|
height={350}
|
||||||
|
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>
|
||||||
|
|
@ -258,7 +320,7 @@ export default function CitizenNews() {
|
||||||
<div key={item.id} className="flex gap-3 items-center">
|
<div key={item.id} className="flex gap-3 items-center">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3 items-center"
|
className="flex gap-3 items-center"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
src={item.thumbnailUrl || "/no-image.jpg"}
|
||||||
|
|
@ -283,11 +345,11 @@ export default function CitizenNews() {
|
||||||
</h2>
|
</h2>
|
||||||
<div className=" w-full">
|
<div className=" w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
articles[0]?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -333,13 +395,13 @@ export default function CitizenNews() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article?.thumbnailUrl ||
|
article?.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
article?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -76,8 +78,8 @@ export default function HeaderCitizen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const citizenArticles = articles.filter((article) =>
|
const citizenArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "berita warga"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -108,9 +110,9 @@ export default function HeaderCitizen() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
||||||
{mainArticle && (
|
{mainArticle && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/detail/${mainArticle.id}`}>
|
<Link href={`/details/${mainArticle.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"}
|
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
||||||
alt={mainArticle.title}
|
alt={mainArticle.title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
|
|
@ -124,7 +126,9 @@ export default function HeaderCitizen() {
|
||||||
{mainArticle.title}
|
{mainArticle.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle.createdByName} -{" "}
|
{mainArticle?.customCreatorName ||
|
||||||
|
mainArticle.createdByName}{" "}
|
||||||
|
-{" "}
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
{new Date(mainArticle.createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
|
|
@ -142,11 +146,11 @@ export default function HeaderCitizen() {
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{otherArticles.map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/detail/${article.id}`}>
|
<Link href={`/details/${article.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.file_url ||
|
article.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type Article = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
categoryName: string;
|
||||||
|
slug: string;
|
||||||
|
createdAt: string;
|
||||||
|
publishedAt: string;
|
||||||
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
categories: { title: string }[];
|
||||||
|
files: { fileUrl: string; file_alt: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Development() {
|
||||||
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
async function initState() {
|
||||||
|
const req = {
|
||||||
|
limit: "10",
|
||||||
|
page,
|
||||||
|
search: "",
|
||||||
|
categorySlug: "",
|
||||||
|
sort: "desc",
|
||||||
|
isPublish: true,
|
||||||
|
sortBy: "created_at",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getListArticle(req);
|
||||||
|
setArticles(res?.data?.data || []);
|
||||||
|
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching articles:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format tanggal ke gaya lokal
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping struktur seperti dummy sebelumnya
|
||||||
|
const leftMain = articles[0];
|
||||||
|
const leftList = articles.slice(1, 4);
|
||||||
|
const centerMain = articles[4];
|
||||||
|
const centerList = articles.slice(5, 8);
|
||||||
|
const rightMain = articles[8];
|
||||||
|
const rightList = articles.slice(9, 12);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="max-w-7xl mx-auto px-4">
|
||||||
|
<div className="bg-white ">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-black text-[#000]">JAGA NEGERI</h2>
|
||||||
|
|
||||||
|
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-b border-gray-300 mb-5"></div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{articles.slice(0, 6).map((item) => (
|
||||||
|
<Link
|
||||||
|
href={`/details/${item.slug}`}
|
||||||
|
key={item.id}
|
||||||
|
className="flex gap-4 pb-4 border-b border-gray-200"
|
||||||
|
>
|
||||||
|
<div className="relative w-28 h-28 rounded-md overflow-hidden flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/placeholder.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-[11px] font-bold text-black">
|
||||||
|
<span className="border-b-2 border-green-600 pb-[1px]">
|
||||||
|
BERITA OPINI
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="font-semibold text-[15px] leading-tight mt-1 line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-gray-600 mt-2">
|
||||||
|
By{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{item.customCreatorName}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-gray-600">
|
||||||
|
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative h-[140px] w-full overflow-hidden rounded-none my-5">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Berita Utama"
|
||||||
|
fill
|
||||||
|
className="object-fill"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import news from "../news";
|
import news from "../news";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getAdvertise } from "@/service/advertisement";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -13,16 +14,27 @@ type Article = {
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
slug: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Advertise = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placement: string;
|
||||||
|
contentFileUrl: string;
|
||||||
|
redirectLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
const slugToLabel = (slug: string) => {
|
const slugToLabel = (slug: string) => {
|
||||||
const mapping: Record<string, string> = {
|
const mapping: Record<string, string> = {
|
||||||
development: "Pembangunan",
|
development: "Pembangunan",
|
||||||
|
|
@ -48,6 +60,36 @@ export default function DevelopmentNews() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const pathSegments = pathname.split("/").filter(Boolean);
|
const pathSegments = pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStateAdver();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function initStateAdver() {
|
||||||
|
const req = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
isPublish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAdvertise(req);
|
||||||
|
const data: Advertise[] = res?.data?.data || [1];
|
||||||
|
|
||||||
|
// filter iklan dengan placement = "banner"
|
||||||
|
const banner = data.find((ad) => ad.placement === "jumbotron");
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
setBannerAd(banner);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching advertisement:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const categorySlug = pathSegments[1];
|
const categorySlug = pathSegments[1];
|
||||||
const categoryLabel = slugToLabel(categorySlug);
|
const categoryLabel = slugToLabel(categorySlug);
|
||||||
|
|
||||||
|
|
@ -81,8 +123,8 @@ export default function DevelopmentNews() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pembangunanArticles = articles.filter((article) =>
|
const pembangunanArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "pembangunan"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -111,7 +153,7 @@ export default function DevelopmentNews() {
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<Link
|
<Link
|
||||||
className="flex flex-col md:flex-row gap-6"
|
className="flex flex-col md:flex-row gap-6"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
{/* Image + Category */}
|
{/* Image + Category */}
|
||||||
<div className="relative w-full md:w-1/2 h-64">
|
<div className="relative w-full md:w-1/2 h-64">
|
||||||
|
|
@ -134,7 +176,7 @@ export default function DevelopmentNews() {
|
||||||
<div className="text-sm text-gray-600 mt-2">
|
<div className="text-sm text-gray-600 mt-2">
|
||||||
BY{" "}
|
BY{" "}
|
||||||
<span className="text-green-600 font-semibold">
|
<span className="text-green-600 font-semibold">
|
||||||
{item.createdByName || "Admin"}
|
{item?.customCreatorName || item.createdByName}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,13 +245,33 @@ 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="/advertisiment.png"
|
src={bannerAd.contentFileUrl}
|
||||||
alt="Advertisement"
|
alt={bannerAd.title || "Iklan Banner"}
|
||||||
fill
|
width={1200} // ukuran dasar untuk responsive
|
||||||
className="object-cover rounded"
|
height={350}
|
||||||
|
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>
|
||||||
|
|
@ -257,7 +319,7 @@ export default function DevelopmentNews() {
|
||||||
<div key={item.id} className="flex gap-3 items-center">
|
<div key={item.id} className="flex gap-3 items-center">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3 items-center"
|
className="flex gap-3 items-center"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
src={item.thumbnailUrl || "/no-image.jpg"}
|
||||||
|
|
@ -282,11 +344,11 @@ export default function DevelopmentNews() {
|
||||||
</h2>
|
</h2>
|
||||||
<div className=" w-full">
|
<div className=" w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
articles[0]?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -332,13 +394,13 @@ export default function DevelopmentNews() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article?.thumbnailUrl ||
|
article?.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
article?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -70,19 +72,21 @@ export default function HeaderDevelopment() {
|
||||||
const res = await getListArticle(req);
|
const res = await getListArticle(req);
|
||||||
setArticles(res?.data?.data || []);
|
setArticles(res?.data?.data || []);
|
||||||
setTotalPage(res?.data?.meta?.totalPage || 1);
|
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||||
|
console.log("develo", res?.data?.data || []);
|
||||||
} finally {
|
} finally {
|
||||||
// close();
|
// close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pembangunanArticles = articles.filter((article) =>
|
const pembangunanArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "pembangunan"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainArticle = pembangunanArticles[0];
|
const mainArticle = pembangunanArticles[0];
|
||||||
const otherArticles = pembangunanArticles.slice(1, 3);
|
const otherArticles = pembangunanArticles.slice(1, 3);
|
||||||
|
console.log("otherArticles:", otherArticles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-7xl mx-auto bg-white">
|
<section className="max-w-7xl mx-auto bg-white">
|
||||||
|
|
@ -112,9 +116,9 @@ export default function HeaderDevelopment() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
||||||
{mainArticle && (
|
{mainArticle && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/detail/${mainArticle.id}`}>
|
<Link href={`/details/${mainArticle.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"}
|
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
||||||
alt={mainArticle.title}
|
alt={mainArticle.title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
|
|
@ -128,7 +132,9 @@ export default function HeaderDevelopment() {
|
||||||
{mainArticle.title}
|
{mainArticle.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle.createdByName} -{" "}
|
{mainArticle?.customCreatorName ||
|
||||||
|
mainArticle.createdByName}{" "}
|
||||||
|
-{" "}
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
{new Date(mainArticle.createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
|
|
@ -146,11 +152,11 @@ export default function HeaderDevelopment() {
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{otherArticles.map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/detail/${article.id}`}>
|
<Link href={`/details/${article.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.file_url ||
|
article.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,66 @@
|
||||||
// components/Footer.tsx
|
// components/Footer.tsx
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import { Facebook, Twitter, Instagram, Youtube } from "lucide-react";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className=" text-[#FFFFFFCC] text-sm font-sans ">
|
<footer className="bg-[#ECEFF5] pt-20 pb-10 w-full">
|
||||||
<div className="max-w-7xl mx-auto py-10 bg-[#09282C] px-8">
|
<div className="max-w-screen-xl mx-auto px-6 grid grid-cols-1 md:grid-cols-2 ">
|
||||||
{/* Top Menu Links */}
|
{/* Logo */}
|
||||||
<div className="flex flex-col md:flex-row justify-center md:justify-between gap-3">
|
<div className="flex justify-center md:justify-end">
|
||||||
{/* <div className="w-full md:w-2/12">
|
|
||||||
<p className="text-sm text-gray-400 mt-5">
|
|
||||||
© 2025{" "}
|
|
||||||
<span className="text-xs text-white font-semibold">JNews</span>-
|
|
||||||
Premium WordPress news & magazine theme by{" "}
|
|
||||||
<span className="text-white font-semibold">Jegtheme</span>
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
<div className="flex items-center overflow-hidden mb-4 py-6 px-8">
|
|
||||||
<Image
|
<Image
|
||||||
src="/mikul.png"
|
src="/mikul-news-logo.png"
|
||||||
alt="Background"
|
alt="Logo"
|
||||||
width={272}
|
width={230}
|
||||||
height={90}
|
height={230}
|
||||||
className="w-full md:w-[272px] h-[90px] object-cover border"
|
className="object-contain"
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-6/12">
|
|
||||||
<h2 className="border-b-2 mb-5"></h2>
|
{/* Subscribe Box */}
|
||||||
<div className="flex items-start flex-wrap justify-start md:justify-start gap-2 md:gap-3 text-xs text-white font-semibold">
|
<div className="flex justify-center md:justify-end">
|
||||||
{[
|
<div className=" p-8 w-full md:w-[420px]">
|
||||||
{ label: "Beranda", href: "#" },
|
<h2 className="text-2xl font-semibold text-gray-800 leading-snug">
|
||||||
{ label: "Pembangunan", href: "/category/development" },
|
Subscribe us to get <br />
|
||||||
{ label: "Kesehatan", href: "/category/health" },
|
the latest news!
|
||||||
{ label: "Berita Warga", href: "/category/citizen-news" },
|
</h2>
|
||||||
].map((item, idx, arr) => (
|
|
||||||
<span
|
<label className="block mt-6 mb-1 text-sm text-gray-600">
|
||||||
key={idx}
|
Email address:
|
||||||
className="flex items-center gap-2 whitespace-nowrap"
|
</label>
|
||||||
>
|
|
||||||
<a href={item.href} className="hover:underline">
|
<input
|
||||||
{item.label}
|
type="email"
|
||||||
</a>
|
placeholder="Your email address"
|
||||||
{idx !== arr.length - 1 && (
|
className="w-full border border-gray-300 rounded-md px-4 py-3 outline-none"
|
||||||
<span className="text-white">/</span>
|
/>
|
||||||
)}
|
|
||||||
</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className=" w-full md:w-3/12">
|
<div className="flex flex-wrap justify-center gap-8 mt-16 text-gray-600 text-sm">
|
||||||
<div className="flex flex-col justify-center md:justify-end gap-2 md:gap-3 text-xs text-[#FFFFFF]">
|
<a href="#">About Us</a>
|
||||||
<p className="text-xs font-bold text-red-600 mb-2 md:mb-0 w-10/12 text-start">
|
<a href="#">Contact</a>
|
||||||
Follow Us
|
<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>
|
||||||
<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,158 +11,222 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
publishedAt: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: { title: string }[];
|
||||||
title: string;
|
files: { fileUrl: string; file_alt: string }[];
|
||||||
}[];
|
|
||||||
files: {
|
|
||||||
file_url: string;
|
|
||||||
file_alt: string;
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HeroNewsSection() {
|
export default function Header() {
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPage, setTotalPage] = useState(1);
|
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [showData, setShowData] = useState("5");
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initState();
|
const fetchArticles = async () => {
|
||||||
}, [page, showData, startDateValue, selectedCategories]);
|
|
||||||
|
|
||||||
async function initState() {
|
|
||||||
// loading();
|
|
||||||
const req = {
|
const req = {
|
||||||
limit: showData,
|
limit: "10",
|
||||||
page,
|
page: 1,
|
||||||
search,
|
search: "",
|
||||||
categorySlug: Array.from(selectedCategories).join(","),
|
categorySlug: "",
|
||||||
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 {
|
|
||||||
// close();
|
fetchArticles();
|
||||||
}
|
}, []);
|
||||||
}
|
|
||||||
|
const flashArticles = articles.slice(0, 8);
|
||||||
|
const mainArticle = articles[8] || articles[0];
|
||||||
|
const recentPosts = articles.slice(1, 5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-7xl mx-auto bg-white">
|
<section className="max-w-7xl mx-auto bg-white px-4">
|
||||||
<div className="flex items-center bg-[#F2F4F3] w-full overflow-hidden mb-4 py-6 px-8">
|
{/* FLASH STRIP */}
|
||||||
<Image
|
<div className="flex items-center justify-between mt-6 mb-3">
|
||||||
src="/mikul.png"
|
<div className="flex items-center gap-3">
|
||||||
alt="Background"
|
<h4 className="text-green-600 font-semibold">Flash</h4>
|
||||||
width={272}
|
<span className="text-red-500">⚡</span>
|
||||||
height={90}
|
</div>
|
||||||
className="w-full md:w-[272px] h-[90px] object-cover border"
|
<div className="text-xs text-gray-500 hidden md:block">LOAD MORE ➜</div>
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pb-5">
|
<div className="overflow-x-auto no-scrollbar py-2">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-2 m-8 ">
|
<div className="flex gap-4">
|
||||||
{articles.length > 0 && (
|
{flashArticles.map((item) => (
|
||||||
<div className="md:col-span-2 lg:col-span-3 relative">
|
<Link
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
href={`/details/${item.slug}`}
|
||||||
<Image
|
key={`flash-${item.id}`}
|
||||||
src={
|
className="min-w-[200px] md:min-w-[220px] bg-gray-800 rounded-lg overflow-hidden relative shadow"
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
|
||||||
"/default-image.jpg"
|
|
||||||
}
|
|
||||||
alt={articles[0].title}
|
|
||||||
width={800}
|
|
||||||
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">
|
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
|
||||||
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
|
||||||
</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">
|
|
||||||
{articles[0].title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-white text-xs flex items-center gap-2">
|
|
||||||
{articles[0].createdByName} -{" "}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<g fill="none">
|
<div className="relative w-[200px] md:w-[220px] h-[140px]">
|
||||||
<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="md:col-span-1 lg:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
||||||
{articles.slice(1, 5).map((article, index) => (
|
|
||||||
<div key={index} className="relative">
|
|
||||||
<Link href={`/detail/${article?.id}`}>
|
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
item.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
item.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/placeholder.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={item.title}
|
||||||
width={400}
|
fill
|
||||||
height={240}
|
className="object-cover"
|
||||||
className="w-full h-56 object-cover"
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
</div>
|
||||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
|
||||||
{article?.categories?.[0]?.title || "TANPA KATEGORI"}
|
{/* dark overlay with text */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent text-white">
|
||||||
|
<p className="text-xs line-clamp-2">{item.title}</p>
|
||||||
|
<div className="flex items-center justify-between mt-2 text-[11px] text-gray-300">
|
||||||
|
<span className="text-yellow-300 bg-black/30 px-1 rounded-sm">
|
||||||
|
{item.categoryName ||
|
||||||
|
item.categories?.[0]?.title ||
|
||||||
|
"Berita"}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
<span>●</span>
|
||||||
{article.title}
|
</div>
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8">
|
{/* Main Layout */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-[2.2fr_1fr] gap-6">
|
||||||
|
{/* LEFT SIDE – MAIN ARTICLE */}
|
||||||
|
{mainArticle ? (
|
||||||
|
<div className="relative h-[500px] w-full rounded-xl overflow-hidden shadow-md">
|
||||||
|
<Link href={`/details/${mainArticle.slug}`}>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
mainArticle.thumbnailUrl ||
|
||||||
|
mainArticle.files?.[0]?.fileUrl ||
|
||||||
|
"/placeholder.jpg"
|
||||||
|
}
|
||||||
|
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* White Card Overlay */}
|
||||||
|
<div className="absolute bottom-6 left-6 bg-white bg-opacity-95 backdrop-blur-sm p-6 shadow-lg max-w-lg">
|
||||||
|
<span className="text-[11px] bg-green-700 text-white px-2 py-1 rounded-sm">
|
||||||
|
{mainArticle.categoryName ||
|
||||||
|
mainArticle.categories?.[0]?.title ||
|
||||||
|
"Berita"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mt-2 leading-snug">
|
||||||
|
{mainArticle.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-gray-600 text-xs mt-3">
|
||||||
|
<span>
|
||||||
|
By{" "}
|
||||||
|
{mainArticle.customCreatorName ||
|
||||||
|
mainArticle.createdByName ||
|
||||||
|
"Admin"}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>
|
||||||
|
{new Date(mainArticle.publishedAt).toLocaleDateString(
|
||||||
|
"id-ID",
|
||||||
|
{
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Loading...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* RIGHT SIDE – RECENT POSTS */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Recent Posts</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recentPosts.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/details/${item.slug}`}
|
||||||
|
className="flex gap-3"
|
||||||
|
>
|
||||||
|
<div className="relative w-35 h-25 rounded-md overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
item.thumbnailUrl ||
|
||||||
|
item.files?.[0]?.fileUrl ||
|
||||||
|
"/placeholder.jpg"
|
||||||
|
}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-sm font-semibold line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<span className="text-xs text-gray-500 mt-1">
|
||||||
|
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LOAD MORE */}
|
||||||
|
<div className="flex justify-center my-6">
|
||||||
|
<button className="text-gray-600 text-sm flex items-center gap-2 border-b pb-1">
|
||||||
|
<span>LOAD MORE</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path
|
||||||
|
d="M12 5v14M5 12h14"
|
||||||
|
stroke="#9CA3AF"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KOLOM PPS BOTTOM BANNER */}
|
||||||
|
<div className="relative h-[140px] w-full overflow-hidden rounded-none my-5 border rounded-md">
|
||||||
<Image
|
<Image
|
||||||
src="/image-kolom.png"
|
src="/image-kolom.png"
|
||||||
alt="Berita Utama"
|
alt="Kolom PPS Bottom Banner"
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getListArticle } from "@/service/article";
|
||||||
|
|
||||||
|
type Article = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
categoryName: string;
|
||||||
|
createdAt: string;
|
||||||
|
publishedAt: string;
|
||||||
|
slug: string;
|
||||||
|
createdByName: string;
|
||||||
|
customCreatorName?: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
categories?: { title: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NewsTerkini() {
|
||||||
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
const [popular, setPopular] = useState<Article[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getListArticle({
|
||||||
|
limit: "20",
|
||||||
|
page: 1,
|
||||||
|
search: "",
|
||||||
|
isPublish: true,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = res?.data?.data || [];
|
||||||
|
setArticles(data.slice(0, 5));
|
||||||
|
setPopular(data.slice(0, 5));
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (d: string) =>
|
||||||
|
new Date(d).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<p className="text-center py-10 text-gray-500">
|
||||||
|
Memuat berita terbaru...
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="max-w-7xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">BERITA TERKINI</h2>
|
||||||
|
<div className="w-14 h-1 bg-green-600 mt-1 mb-4"></div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{articles.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/details/${item.slug}`}
|
||||||
|
className="block border-b pb-6"
|
||||||
|
>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
{/* CATEGORY */}
|
||||||
|
<p className="text-[11px] text-green-700 font-semibold mb-1">
|
||||||
|
{item.categoryName || "Kategori"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* JUDUL */}
|
||||||
|
<h3 className="font-bold text-base leading-snug line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* DESKRIPSI */}
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2 mt-1">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* AUTHOR + DATE */}
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
By {item.customCreatorName || item.createdByName} —{" "}
|
||||||
|
{formatDate(item.publishedAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-40 h-28 rounded overflow-hidden flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/placeholder.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LOAD MORE */}
|
||||||
|
<div className="text-center mt-4 text-green-600 text-sm cursor-pointer">
|
||||||
|
LOAD MORE ↓
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">TERBANYAK DIBAGIKAN</h2>
|
||||||
|
<div className="w-14 h-1 bg-green-600 mt-1 mb-4"></div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{popular.map((item, index) => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/details/${item.slug}`}
|
||||||
|
className="flex gap-3 border-b pb-4"
|
||||||
|
>
|
||||||
|
{/* NOMOR */}
|
||||||
|
<div className="text-green-600 font-extrabold text-3xl leading-none">
|
||||||
|
{(index + 1).toString().padStart(2, "0")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-[10px] text-gray-500">
|
||||||
|
{item.categories?.[0]?.title || "Kategori"}
|
||||||
|
</p>
|
||||||
|
<h4 className="font-semibold text-sm leading-snug line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[10px] text-gray-400 mt-1">
|
||||||
|
{formatDate(item.createdAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* THUMBNAIL KECIL */}
|
||||||
|
<div className="relative w-16 h-14 rounded overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/placeholder.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="relative h-[180px] border rounded-lg overflow-hidden mb-6">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Kolom PPS"
|
||||||
|
fill
|
||||||
|
className="object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative h-[180px] border rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Kolom PPS"
|
||||||
|
fill
|
||||||
|
className="object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -12,13 +12,15 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -76,8 +78,8 @@ export default function HeaderHealth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthArticles = articles.filter((article) =>
|
const healthArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "kesehatan"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -108,9 +110,9 @@ export default function HeaderHealth() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 m-8">
|
||||||
{mainArticle && (
|
{mainArticle && (
|
||||||
<div className="md:col-span-2 relative">
|
<div className="md:col-span-2 relative">
|
||||||
<Link href={`/detail/${mainArticle.id}`}>
|
<Link href={`/details/${mainArticle.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.files?.[0]?.file_url || "/default-image.jpg"}
|
src={mainArticle.files?.[0]?.fileUrl || "/default-image.jpg"}
|
||||||
alt={mainArticle.title}
|
alt={mainArticle.title}
|
||||||
width={800}
|
width={800}
|
||||||
height={500}
|
height={500}
|
||||||
|
|
@ -124,7 +126,9 @@ export default function HeaderHealth() {
|
||||||
{mainArticle.title}
|
{mainArticle.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-xs">
|
<p className="text-white text-xs">
|
||||||
{mainArticle.createdByName} -{" "}
|
{mainArticle?.customCreatorName ||
|
||||||
|
mainArticle.createdByName}{" "}
|
||||||
|
-{" "}
|
||||||
{new Date(mainArticle.createdAt).toLocaleDateString(
|
{new Date(mainArticle.createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
|
|
@ -142,11 +146,11 @@ export default function HeaderHealth() {
|
||||||
<div className="grid grid-rows-2 gap-2">
|
<div className="grid grid-rows-2 gap-2">
|
||||||
{otherArticles.map((article, index) => (
|
{otherArticles.map((article, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
<Link href={`/detail/${article.id}`}>
|
<Link href={`/details/${article.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article.thumbnailUrl ||
|
article.thumbnailUrl ||
|
||||||
article.files?.[0]?.file_url ||
|
article.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import news from "../news";
|
import news from "../news";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getAdvertise } from "@/service/advertisement";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -12,13 +13,15 @@ type Article = {
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -32,6 +35,15 @@ const slugToLabel = (slug: string) => {
|
||||||
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
|
return mapping[slug] || slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Advertise = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placement: string;
|
||||||
|
contentFileUrl: string;
|
||||||
|
redirectLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function HealthNews() {
|
export default function HealthNews() {
|
||||||
const [activeTab, setActiveTab] = useState("comments");
|
const [activeTab, setActiveTab] = useState("comments");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
@ -48,6 +60,36 @@ export default function HealthNews() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const pathSegments = pathname.split("/").filter(Boolean);
|
const pathSegments = pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStateAdver();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function initStateAdver() {
|
||||||
|
const req = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
isPublish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAdvertise(req);
|
||||||
|
const data: Advertise[] = res?.data?.data || [1];
|
||||||
|
|
||||||
|
// filter iklan dengan placement = "banner"
|
||||||
|
const banner = data.find((ad) => ad.placement === "jumbotron");
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
setBannerAd(banner);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching advertisement:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const categorySlug = pathSegments[1];
|
const categorySlug = pathSegments[1];
|
||||||
const categoryLabel = slugToLabel(categorySlug);
|
const categoryLabel = slugToLabel(categorySlug);
|
||||||
|
|
||||||
|
|
@ -81,11 +123,10 @@ export default function HealthNews() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const kesehatanArticles = articles.filter((article) =>
|
const kesehatanArticles = articles.filter((article) =>
|
||||||
article.categories?.some(
|
article.categories?.some((category) =>
|
||||||
(category) => category.title.toLowerCase() === "kesehatan"
|
category.title?.toLowerCase().includes("berita warga")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemsPerPage = 2;
|
const itemsPerPage = 2;
|
||||||
const calculatedTotalPage = Math.ceil(
|
const calculatedTotalPage = Math.ceil(
|
||||||
kesehatanArticles.length / itemsPerPage
|
kesehatanArticles.length / itemsPerPage
|
||||||
|
|
@ -109,7 +150,7 @@ export default function HealthNews() {
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<Link
|
<Link
|
||||||
className="flex flex-col md:flex-row gap-6"
|
className="flex flex-col md:flex-row gap-6"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-full md:w-1/2 h-64">
|
<div className="relative w-full md:w-1/2 h-64">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -131,7 +172,7 @@ export default function HealthNews() {
|
||||||
<div className="text-sm text-gray-600 mt-2">
|
<div className="text-sm text-gray-600 mt-2">
|
||||||
BY{" "}
|
BY{" "}
|
||||||
<span className="text-green-600 font-semibold">
|
<span className="text-green-600 font-semibold">
|
||||||
{item.createdByName || "Admin"}
|
{item?.customCreatorName || item.createdByName || "Admin"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
• {new Date(item.createdAt).toLocaleDateString("id-ID")}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -197,13 +238,33 @@ export default function HealthNews() {
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="w-full h-[400px] relative">
|
<div className="w-full h-[400px] relative">
|
||||||
|
{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="/advertisiment.png"
|
src={bannerAd.contentFileUrl}
|
||||||
alt="Advertisement"
|
alt={bannerAd.title || "Iklan Banner"}
|
||||||
fill
|
width={1200} // ukuran dasar untuk responsive
|
||||||
className="object-cover rounded"
|
height={350}
|
||||||
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-2">Connect with us</h3>
|
<h3 className="text-lg font-semibold mb-2">Connect with us</h3>
|
||||||
|
|
@ -249,7 +310,7 @@ export default function HealthNews() {
|
||||||
<div key={item.id} className="flex gap-3 items-center">
|
<div key={item.id} className="flex gap-3 items-center">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3 items-center"
|
className="flex gap-3 items-center"
|
||||||
href={`/detail/${item?.id}`}
|
href={`/details/${item?.slug}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={item.thumbnailUrl || "/no-image.jpg"}
|
src={item.thumbnailUrl || "/no-image.jpg"}
|
||||||
|
|
@ -274,11 +335,11 @@ export default function HealthNews() {
|
||||||
</h2>
|
</h2>
|
||||||
<div className=" w-full">
|
<div className=" w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
articles[0]?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -324,13 +385,13 @@ export default function HealthNews() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article?.thumbnailUrl ||
|
article?.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
article?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
|
||||||
|
|
@ -171,14 +171,17 @@ type Article = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
|
publishedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
slug: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -237,7 +240,7 @@ export default function LatestandPopular() {
|
||||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
const currentArticles = articles.slice(
|
const currentArticles = articles.slice(
|
||||||
startIndex,
|
startIndex,
|
||||||
startIndex + ITEMS_PER_PAGE
|
startIndex + ITEMS_PER_PAGE,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<section className="bg-white py-10 px-4 md:px-10 w-full">
|
<section className="bg-white py-10 px-4 md:px-10 w-full">
|
||||||
|
|
@ -250,12 +253,12 @@ export default function LatestandPopular() {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
{currentArticles.map((article, index) => (
|
{currentArticles.map((article, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Link href={`/detail/${article?.id}`}>
|
<Link href={`/details/${article?.slug}`}>
|
||||||
<div className="relative w-full aspect-video mb-3">
|
<div className="relative w-full aspect-video mb-3">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article?.thumbnailUrl ||
|
article?.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
article?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={article?.title || "No title"}
|
alt={article?.title || "No title"}
|
||||||
|
|
@ -280,7 +283,7 @@ export default function LatestandPopular() {
|
||||||
<div className="text-xs text-[#999999] mb-2 flex items-center gap-2">
|
<div className="text-xs text-[#999999] mb-2 flex items-center gap-2">
|
||||||
by{" "}
|
by{" "}
|
||||||
<span className="text-[#31942E]">
|
<span className="text-[#31942E]">
|
||||||
{article.createdByName}
|
{article?.customCreatorName || article.createdByName}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
|{" "}
|
|{" "}
|
||||||
<div className="text-xs mt-1.5 text-[#A0A0A0] space-x-2 flex items-center">
|
<div className="text-xs mt-1.5 text-[#A0A0A0] space-x-2 flex items-center">
|
||||||
|
|
@ -299,14 +302,18 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(article?.createdAt).toLocaleDateString(
|
{new Date(article?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
|
|
@ -367,11 +374,11 @@ export default function LatestandPopular() {
|
||||||
</h2>
|
</h2>
|
||||||
<div className=" w-full">
|
<div className=" w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.thumbnailUrl ||
|
articles[0]?.thumbnailUrl ||
|
||||||
articles[0]?.files?.[0]?.file_url ||
|
articles[0]?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
|
|
@ -399,14 +406,18 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -417,13 +428,13 @@ export default function LatestandPopular() {
|
||||||
<div key={index} className="flex gap-3">
|
<div key={index} className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
article?.thumbnailUrl ||
|
article?.thumbnailUrl ||
|
||||||
article?.files?.[0]?.file_url ||
|
article?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
|
|
@ -450,14 +461,18 @@ export default function LatestandPopular() {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getListArticle } from "@/service/article";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Article = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
categoryName: string;
|
||||||
|
slug: string;
|
||||||
|
createdAt: string;
|
||||||
|
publishedAt: string;
|
||||||
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
categories: { title: string }[];
|
||||||
|
files: { fileUrl: string; file_alt: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function News() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
const [showData, setShowData] = useState("6");
|
||||||
|
const [search] = useState("");
|
||||||
|
const [selectedCategories] = useState<any>("");
|
||||||
|
const [startDateValue] = useState({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, [page, showData, startDateValue, selectedCategories]);
|
||||||
|
|
||||||
|
async function initState() {
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
|
sort: "desc",
|
||||||
|
isPublish: true,
|
||||||
|
sortBy: "created_at",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getListArticle(req);
|
||||||
|
setArticles(res?.data?.data || []);
|
||||||
|
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching articles:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="max-w-screen-xl mx-auto px-4 py-10">
|
||||||
|
<div className="">
|
||||||
|
{/* TITLE */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-black text-[#000]">BERITA POPULER</h2>
|
||||||
|
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* GRID 4 KOLOM */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
{articles.slice(0, 4).map((item) => (
|
||||||
|
<Link href={`/details/${item.slug}`} key={item.id}>
|
||||||
|
<div>
|
||||||
|
{/* GAMBAR */}
|
||||||
|
<div className="relative w-full h-56 rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/placeholder.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* BADGE CATEGORY DI DALAM GAMBAR */}
|
||||||
|
<div className="absolute bottom-2 left-2">
|
||||||
|
<span className="px-3 py-1 text-[10px] font-semibold bg-green-600 text-white rounded">
|
||||||
|
{item.categoryName || "Kategori"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JUDUL */}
|
||||||
|
<h3 className="mt-2 text-base font-bold leading-snug line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* AUTHOR + DATE */}
|
||||||
|
<div className="text-[11px] mt-2 flex items-center gap-1 text-gray-700">
|
||||||
|
<span className="font-semibold">
|
||||||
|
By {item.customCreatorName || item.createdByName || "Admin"}
|
||||||
|
</span>
|
||||||
|
<span className="text-yellow-500">-</span>
|
||||||
|
<span>
|
||||||
|
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative h-[160px] w-full overflow-hidden rounded-xl mt-6">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Kolom PPS"
|
||||||
|
fill
|
||||||
|
className="object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -176,15 +176,17 @@ type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
publishedAt: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
@ -249,9 +251,9 @@ export default function Latest({ id }: { id: number }) {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 ">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 ">
|
||||||
<div className=" w-full">
|
<div className=" w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={articles[0]?.files?.[0]?.file_url || "/nodata.png"}
|
src={articles[0]?.files?.[0]?.fileUrl || "/nodata.png"}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||||
|
|
@ -277,14 +279,18 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -295,7 +301,7 @@ export default function Latest({ id }: { id: number }) {
|
||||||
<div key={index} className="flex gap-3">
|
<div key={index} className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -324,14 +330,18 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -342,10 +352,10 @@ export default function Latest({ id }: { id: number }) {
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative w-full aspect-video mb-5">
|
<div className="relative w-full aspect-video mb-5">
|
||||||
<Link href={`/detail/${articles[0]?.id}`}>
|
<Link href={`/details/${articles[0]?.slug}`}>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
articles[0]?.files?.[0]?.file_url || "/default-image.jpg"
|
articles[0]?.files?.[0]?.fileUrl || "/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={"articles[0]?.title"}
|
alt={"articles[0]?.title"}
|
||||||
fill
|
fill
|
||||||
|
|
@ -372,14 +382,18 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -390,7 +404,7 @@ export default function Latest({ id }: { id: number }) {
|
||||||
<div key={index} className="flex gap-3">
|
<div key={index} className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3"
|
className="flex gap-3"
|
||||||
href={`/detail/${article?.id}`}
|
href={`/details/${article?.slug}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -419,14 +433,18 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -474,10 +492,10 @@ export default function Latest({ id }: { id: number }) {
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{popularPosts.slice(1, 5).map((post, index) => (
|
{popularPosts.slice(1, 5).map((post, index) => (
|
||||||
<div key={index} className="space-y-3">
|
<div key={index} className="space-y-3">
|
||||||
<Link href={`/detail/${post?.id}`}>
|
<Link href={`/details/${post?.slug}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex gap-4 ${
|
className={`flex gap-4 ${
|
||||||
post.files?.[0]?.file_url
|
post.files?.[0]?.fileUrl
|
||||||
? "flex-col md:flex-row"
|
? "flex-col md:flex-row"
|
||||||
: "flex-col"
|
: "flex-col"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -501,14 +519,18 @@ export default function Latest({ id }: { id: number }) {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
{new Date(articles[0]?.publishedAt)
|
||||||
"id-ID",
|
.toLocaleString("id-ID", {
|
||||||
{
|
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
hour: "2-digit",
|
||||||
)}
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,36 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Menu, Lock, Search } from "lucide-react";
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const Navbar = () => {
|
export default function Navbar() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const toggleMenu = () => setIsOpen(!isOpen);
|
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const isActive = (href: any) => {
|
||||||
const navLinks = [
|
return pathname === href || pathname.startsWith(href + "/");
|
||||||
{ href: "/", label: "BERANDA" },
|
|
||||||
{ href: "/category/development", label: "PEMBANGUNAN" },
|
|
||||||
{ href: "/category/health", label: "KESEHATAN" },
|
|
||||||
{ href: "/category/citizen-news", label: "BERITA WARGA" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const isActive = (href: string) => {
|
|
||||||
if (href === "/") {
|
|
||||||
return pathname === "/";
|
|
||||||
}
|
|
||||||
return pathname.startsWith(href);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="w-full bg-white shadow-md">
|
<div className="w-full bg-white py-4 border-b">
|
||||||
<div className="max-w-7xl mx-auto py-2 px-0 md:px-4 flex flex-col md:flex-row justify-end items-start md:justify-between md:items-center gap-4 bg-[#308A2E]">
|
<div className="max-w-screen-xl mx-auto flex flex-col justify-between px-4">
|
||||||
<div className="flex items-center gap-4 w-full md:w-auto mx-3 md:mx-5">
|
{/* Left: Logo */}
|
||||||
<div className="flex gap-3 text-xs mx-3 text-white">
|
<div className="flex flex-row justify-between mb-3">
|
||||||
<Link href="/about" className="hover:text-yellow-400 ">
|
<div className="flex items-center">
|
||||||
About
|
<Image
|
||||||
</Link>
|
src="/mikul-news-logo.png"
|
||||||
<Link href="/advertise" className="hover:text-yellow-400">
|
alt="Kritik Tajam Logo"
|
||||||
Advertise
|
width={140}
|
||||||
</Link>
|
height={100}
|
||||||
<Link href="/privacy" className="hover:text-yellow-400">
|
/>
|
||||||
Privacy & Policy
|
|
||||||
</Link>
|
|
||||||
<Link href="/contact" className="hover:text-yellow-400">
|
|
||||||
Contact
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-6">
|
||||||
|
{/* Social Icons */}
|
||||||
<div className="flex items-center gap-5 text-sm whitespace-nowrap text-white mx-5">
|
<div className="hidden md:flex items-center gap-5 text-black text-xl">
|
||||||
<div className="hidden md:block min-w-fit whitespace-nowrap text-white text-xs">
|
<Link href="#">
|
||||||
{new Date().toLocaleDateString("id-ID", {
|
|
||||||
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="15"
|
width="18"
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -67,11 +39,11 @@ const 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="15"
|
width="18"
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -80,150 +52,119 @@ const 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="15"
|
width="18"
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
// fill-rule="evenodd"
|
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"
|
||||||
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">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
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">
|
<Link href="#">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="15"
|
width="18"
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
// fill-rule="evenodd"
|
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"
|
||||||
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>
|
||||||
|
</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
|
<Link
|
||||||
href="/auth"
|
href="/category/citizen-news"
|
||||||
className="hover:underline flex items-center gap-1"
|
className={
|
||||||
|
isActive("/category/citizen-news")
|
||||||
|
? "text-green-500 underline"
|
||||||
|
: "text-black hover:text-green-500"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Lock className="w-3 h-3" />
|
Berita Warga
|
||||||
Login
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-[#31942E] text-white">
|
<Link
|
||||||
<div className="flex items-start justify-start md:justify-center md:items-center px-4 py-3 md:px-8 md:py-4">
|
href="/category/development"
|
||||||
{/* Toggle Menu (Mobile Only) */}
|
className={
|
||||||
<button
|
isActive("/category/development")
|
||||||
className="md:hidden flex items-center"
|
? "text-green-500 underline"
|
||||||
onClick={toggleMenu}
|
: "text-black hover:text-green-500"
|
||||||
aria-label="Toggle menu"
|
}
|
||||||
>
|
>
|
||||||
<Menu className="h-6 w-6 text-white" />
|
Pembangunan
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/category/health"
|
||||||
|
className={
|
||||||
|
isActive("/category/health")
|
||||||
|
? "text-green-500 underline"
|
||||||
|
: "text-black hover:text-green-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Kesehatan
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
// onClick={() => document.documentElement.classList.toggle("dark")}
|
||||||
|
className="w-10 h-5 rounded-full bg-gray-300 dark:bg-gray-700 relative transition-all"
|
||||||
|
>
|
||||||
|
<div className="w-5 h-5 bg-white dark:bg-black rounded-full shadow absolute top-0 left-0 dark:left-5 transition-all"></div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Navigation Links (Desktop Only) */}
|
{/* BURGER BUTTON (mobile menu) */}
|
||||||
<div className="hidden md:flex items-center md:gap-x-32 font-semibold text-sm">
|
<button className="md:hidden p-2 rounded-lg border">
|
||||||
{navLinks.map((link, idx) => (
|
<svg
|
||||||
<Link
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
key={idx}
|
fill="none"
|
||||||
href={link.href}
|
viewBox="0 0 24 24"
|
||||||
className={`pl-4 pr-4 ${
|
strokeWidth={2}
|
||||||
isActive(link.href)
|
stroke="currentColor"
|
||||||
? "text-yellow-400"
|
className="w-6 h-6"
|
||||||
: "hover:text-yellow-400 text-white"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{link.label}
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 border rounded-full">
|
||||||
|
<Search size={15} />
|
||||||
|
</button>
|
||||||
|
<Link href={"/auth"}>
|
||||||
|
<button className="bg-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,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { getAdvertise } from "@/service/advertisement";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
import { Clock } from "lucide-react";
|
import { Clock } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
@ -9,19 +10,31 @@ type postsData = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
publishedAt: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
slug: string;
|
||||||
createdByName: string;
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
categories: {
|
categories: {
|
||||||
title: string;
|
title: string;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
file_url: string;
|
fileUrl: string;
|
||||||
file_alt: string;
|
file_alt: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Advertise = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placement: string;
|
||||||
|
contentFileUrl: string;
|
||||||
|
redirectLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Beranda() {
|
export default function Beranda() {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPage, setTotalPage] = useState(1);
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
|
|
@ -34,6 +47,36 @@ export default function Beranda() {
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [bannerAd, setBannerAd] = useState<Advertise | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStateAdver();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function initStateAdver() {
|
||||||
|
const req = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
sort: "desc",
|
||||||
|
sortBy: "created_at",
|
||||||
|
isPublish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAdvertise(req);
|
||||||
|
const data: Advertise[] = res?.data?.data || [1];
|
||||||
|
|
||||||
|
// filter iklan dengan placement = "banner"
|
||||||
|
const banner = data.find((ad) => ad.placement === "banner");
|
||||||
|
|
||||||
|
if (banner) {
|
||||||
|
setBannerAd(banner);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching advertisement:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initState();
|
initState();
|
||||||
}, [page, showData, startDateValue, selectedCategories]);
|
}, [page, showData, startDateValue, selectedCategories]);
|
||||||
|
|
@ -76,18 +119,18 @@ export default function Beranda() {
|
||||||
{posts
|
{posts
|
||||||
.filter((post) =>
|
.filter((post) =>
|
||||||
post.categories?.some(
|
post.categories?.some(
|
||||||
(category) => category.title.toLowerCase() === "pembangunan"
|
(category) => category.title.toLowerCase() === "pembangunan",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
||||||
.map((post, index) => (
|
.map((post, index) => (
|
||||||
<div key={index} className="bg-white overflow-hidden">
|
<div key={index} className="bg-white overflow-hidden">
|
||||||
<Link href={`/detail/${post?.id}`}>
|
<Link href={`/details/${post?.slug}`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
post.thumbnailUrl ||
|
post.thumbnailUrl ||
|
||||||
post?.files?.[0]?.file_url ||
|
post?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
|
|
@ -107,12 +150,19 @@ export default function Beranda() {
|
||||||
<div className="text-xs flex items-center gap-2">
|
<div className="text-xs flex items-center gap-2">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{post.createdByName} -{" "}
|
{post?.customCreatorName || post.createdByName} -{" "}
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
{new Date(post.publishedAt)
|
||||||
|
.toLocaleString("id-ID", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "Asia/Jakarta",
|
||||||
|
})
|
||||||
|
.replace("pukul ", "")}{" "}
|
||||||
|
WIB
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,13 +171,33 @@ export default function Beranda() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
<div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
|
||||||
|
{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"
|
||||||
fill
|
width={1200}
|
||||||
className="object-cover"
|
height={188}
|
||||||
|
className="object-contain w-full h-[188px]"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-1 gap-2 bg-white border-b-2 pt-2 ">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-1 gap-2 bg-white border-b-2 pt-2 ">
|
||||||
<h2 className="text-sm font-bold">Kesehatan</h2>
|
<h2 className="text-sm font-bold">Kesehatan</h2>
|
||||||
|
|
@ -145,18 +215,18 @@ export default function Beranda() {
|
||||||
{posts
|
{posts
|
||||||
.filter((post) =>
|
.filter((post) =>
|
||||||
post.categories?.some(
|
post.categories?.some(
|
||||||
(category) => category.title.toLowerCase() === "kesehatan"
|
(category) => category.title.toLowerCase() === "kesehatan",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
.slice(0, 6) // Ambil 4 artikel pertama setelah difilter
|
||||||
.map((post, index) => (
|
.map((post, index) => (
|
||||||
<div key={index} className="bg-white overflow-hidden">
|
<div key={index} className="bg-white overflow-hidden">
|
||||||
<Link href={`/detail/${post?.id}`}>
|
<Link href={`/details/${post?.slug}`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
post.thumbnailUrl ||
|
post.thumbnailUrl ||
|
||||||
post?.files?.[0]?.file_url ||
|
post?.files?.[0]?.fileUrl ||
|
||||||
"/default-image.jpg"
|
"/default-image.jpg"
|
||||||
}
|
}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
|
|
@ -176,7 +246,7 @@ export default function Beranda() {
|
||||||
<div className="text-xs flex items-center gap-2">
|
<div className="text-xs flex items-center gap-2">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{post.createdByName} -{" "}
|
{post?.customCreatorName || post.createdByName} -{" "}
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
|
|
@ -190,13 +260,33 @@ export default function Beranda() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
<div className="relative mt-10 mb-2 flex justify-center mx-8 border my-8 h-[350px] overflow-hidden bg-white">
|
||||||
|
{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"
|
||||||
fill
|
width={1200}
|
||||||
className="object-cover"
|
height={188}
|
||||||
|
className="object-contain w-full h-[188px]"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getListArticle } from "@/service/article";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Article = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
categoryName: string;
|
||||||
|
slug: string;
|
||||||
|
createdAt: string;
|
||||||
|
publishedAt: string;
|
||||||
|
createdByName: string;
|
||||||
|
customCreatorName: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
categories: { title: string }[];
|
||||||
|
files: { fileUrl: string; file_alt: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OpinionNews() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
const [showData, setShowData] = useState("6");
|
||||||
|
const [search] = useState("");
|
||||||
|
const [selectedCategories] = useState<any>("");
|
||||||
|
const [startDateValue] = useState({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, [page, showData, startDateValue, selectedCategories]);
|
||||||
|
|
||||||
|
async function initState() {
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
|
sort: "desc",
|
||||||
|
isPublish: true,
|
||||||
|
sortBy: "created_at",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getListArticle(req);
|
||||||
|
setArticles(res?.data?.data || []);
|
||||||
|
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching articles:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="max-w-screen-xl mx-auto px-4 py-10">
|
||||||
|
<div className="">
|
||||||
|
{/* TITLE */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-black text-[#000]">BERITA OPINI</h2>
|
||||||
|
<div className="w-10 h-1 bg-green-600 mt-1 rounded"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* GRID 4 KOLOM */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
{articles.slice(0, 4).map((item) => (
|
||||||
|
<Link href={`/details/${item.slug}`} key={item.id}>
|
||||||
|
<div>
|
||||||
|
{/* GAMBAR */}
|
||||||
|
<div className="relative w-full h-56 rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/placeholder.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* BADGE CATEGORY DI DALAM GAMBAR */}
|
||||||
|
<div className="absolute bottom-2 left-2">
|
||||||
|
<span className="px-3 py-1 text-[10px] font-semibold bg-green-600 text-white rounded">
|
||||||
|
{item.categoryName || "Kategori"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JUDUL */}
|
||||||
|
<h3 className="mt-2 text-base font-bold leading-snug line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* AUTHOR + DATE */}
|
||||||
|
<div className="text-[11px] mt-2 flex items-center gap-1 text-gray-700">
|
||||||
|
<span className="font-semibold">
|
||||||
|
By {item.customCreatorName || item.createdByName || "Admin"}
|
||||||
|
</span>
|
||||||
|
<span className="text-yellow-500">-</span>
|
||||||
|
<span>
|
||||||
|
{new Date(item.publishedAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative h-[160px] w-full overflow-hidden rounded-xl mt-6">
|
||||||
|
<Image
|
||||||
|
src="/image-kolom.png"
|
||||||
|
alt="Kolom PPS"
|
||||||
|
fill
|
||||||
|
className="object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { Eye, Heart, MessageCircle } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface VideoItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
thumbnail: string;
|
||||||
|
duration: string;
|
||||||
|
publishedAt: string;
|
||||||
|
views: string;
|
||||||
|
likes: string;
|
||||||
|
comments: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleVideos: VideoItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Cuplikan Kegiatan Presiden di IKN",
|
||||||
|
thumbnail: "/yt/thumb1.jpg",
|
||||||
|
duration: "12:30",
|
||||||
|
publishedAt: "2 jam yang lalu",
|
||||||
|
views: "12K",
|
||||||
|
likes: "230",
|
||||||
|
comments: "45",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Pembangunan MRT Fase Berikutnya Resmi Dimulai",
|
||||||
|
thumbnail: "/yt/thumb2.jpg",
|
||||||
|
duration: "08:12",
|
||||||
|
publishedAt: "5 jam yang lalu",
|
||||||
|
views: "9.4K",
|
||||||
|
likes: "180",
|
||||||
|
comments: "30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Wilayah Indonesia Siap Hadapi Cuaca Ekstrem",
|
||||||
|
thumbnail: "/yt/thumb3.jpg",
|
||||||
|
duration: "05:50",
|
||||||
|
publishedAt: "1 hari lalu",
|
||||||
|
views: "21K",
|
||||||
|
likes: "540",
|
||||||
|
comments: "121",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Peningkatan Ekonomi Regional Terus Dijaga",
|
||||||
|
thumbnail: "/yt/thumb4.jpg",
|
||||||
|
duration: "10:44",
|
||||||
|
publishedAt: "2 hari lalu",
|
||||||
|
views: "18K",
|
||||||
|
likes: "420",
|
||||||
|
comments: "88",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "Laporan Khusus Perkembangan Industri Kreatif",
|
||||||
|
thumbnail: "/yt/thumb5.jpg",
|
||||||
|
duration: "14:02",
|
||||||
|
publishedAt: "3 hari lalu",
|
||||||
|
views: "30K",
|
||||||
|
likes: "830",
|
||||||
|
comments: "200",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: "Paparan Menteri Perhubungan Soal Transportasi",
|
||||||
|
thumbnail: "/yt/thumb6.jpg",
|
||||||
|
duration: "07:21",
|
||||||
|
publishedAt: "4 hari lalu",
|
||||||
|
views: "9K",
|
||||||
|
likes: "114",
|
||||||
|
comments: "26",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function YouTubeSection() {
|
||||||
|
return (
|
||||||
|
<section className="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
{/* Header Channel */}
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<Image
|
||||||
|
src="/yt-logo.png"
|
||||||
|
alt="Logo Channel"
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold">YouTube • Channel Resmi</h2>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Lebih dari 3.5 juta pengikut • 12k video
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="ml-auto bg-red-600 text-white px-4 py-2 rounded-lg font-semibold"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid Video */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{sampleVideos.map((v) => (
|
||||||
|
<div key={v.id} className="cursor-pointer">
|
||||||
|
<div className="relative w-full h-44">
|
||||||
|
<Image
|
||||||
|
src={v.thumbnail}
|
||||||
|
alt={v.title}
|
||||||
|
fill
|
||||||
|
className="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="absolute bottom-1 right-1 bg-black/80 text-white text-xs px-2 py-1 rounded">
|
||||||
|
{v.duration}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-semibold mt-2 leading-tight line-clamp-2">
|
||||||
|
{v.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600">{v.publishedAt}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-700 mt-1">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Eye size={16} /> {v.views}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Heart size={16} /> {v.likes}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MessageCircle size={16} /> {v.comments}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex justify-center mt-8">
|
||||||
|
<button className="px-5 py-2 rounded-lg border hover:bg-gray-100">
|
||||||
|
Lihat Selanjutnya →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -106,7 +106,7 @@ export default function DashboardContainer() {
|
||||||
|
|
||||||
async function initState() {
|
async function initState() {
|
||||||
const req = {
|
const req = {
|
||||||
limit: "4",
|
limit: "5",
|
||||||
page: page,
|
page: page,
|
||||||
search: "",
|
search: "",
|
||||||
};
|
};
|
||||||
|
|
@ -207,11 +207,15 @@ export default function DashboardContainer() {
|
||||||
<p className="text-slate-600">{username}</p>
|
<p className="text-slate-600">{username}</p>
|
||||||
<div className="flex space-x-6 pt-2">
|
<div className="flex space-x-6 pt-2">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-blue-600">{summary?.totalToday}</p>
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{summary?.totalToday}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">Today</p>
|
<p className="text-sm text-slate-500">Today</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-purple-600">{summary?.totalThisWeek}</p>
|
<p className="text-2xl font-bold text-purple-600">
|
||||||
|
{summary?.totalThisWeek}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">This Week</p>
|
<p className="text-sm text-slate-500">This Week</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -234,7 +238,9 @@ export default function DashboardContainer() {
|
||||||
<DashboardSpeecIcon />
|
<DashboardSpeecIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalAll}</p>
|
<p className="text-3xl font-bold text-slate-800">
|
||||||
|
{summary?.totalAll}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Posts</p>
|
<p className="text-sm text-slate-500">Total Posts</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,7 +258,9 @@ export default function DashboardContainer() {
|
||||||
<DashboardConnectIcon />
|
<DashboardConnectIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalViews}</p>
|
<p className="text-3xl font-bold text-slate-800">
|
||||||
|
{summary?.totalViews}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Views</p>
|
<p className="text-sm text-slate-500">Total Views</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,7 +278,9 @@ export default function DashboardContainer() {
|
||||||
<DashboardShareIcon />
|
<DashboardShareIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalShares}</p>
|
<p className="text-3xl font-bold text-slate-800">
|
||||||
|
{summary?.totalShares}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Shares</p>
|
<p className="text-sm text-slate-500">Total Shares</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -288,7 +298,9 @@ export default function DashboardContainer() {
|
||||||
<DashboardCommentIcon size={40} />
|
<DashboardCommentIcon size={40} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalComments}</p>
|
<p className="text-3xl font-bold text-slate-800">
|
||||||
|
{summary?.totalComments}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Comments</p>
|
<p className="text-sm text-slate-500">Total Comments</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -305,13 +317,20 @@ export default function DashboardContainer() {
|
||||||
transition={{ delay: 0.6 }}
|
transition={{ delay: 0.6 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-lg font-semibold text-slate-800">Analytics Overview</h3>
|
<h3 className="text-lg font-semibold text-slate-800">
|
||||||
|
Analytics Overview
|
||||||
|
</h3>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<label key={option.value} className="flex items-center space-x-2">
|
<label
|
||||||
|
key={option.value}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={analyticsView.includes(option.value)}
|
checked={analyticsView.includes(option.value)}
|
||||||
onCheckedChange={(checked) => handleChange(option.value, checked as boolean)}
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange(option.value, checked as boolean)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-slate-600">{option.label}</span>
|
<span className="text-sm text-slate-600">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -335,12 +354,14 @@ export default function DashboardContainer() {
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.7 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-lg font-semibold text-slate-800">Recent Articles</h3>
|
<h3 className="text-lg font-semibold text-slate-800">
|
||||||
<Link href="/admin/article/create">
|
Recent Articles
|
||||||
|
</h3>
|
||||||
|
{/* <Link href="/admin/article/create">
|
||||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
||||||
Create Article
|
Create Article
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
|
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { close, error, loading, success, successToast } from "@/config/swal";
|
import { close, error, loading, success, successToast } from "@/config/swal";
|
||||||
import { Article } from "@/types/globals";
|
import { Article } from "@/types/globals";
|
||||||
import { convertDateFormat } from "@/utils/global";
|
import { convertDateFormat, formatDate } from "@/utils/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Key, useCallback, useEffect, useState } from "react";
|
import { Key, useCallback, useEffect, useState } from "react";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
|
@ -46,6 +46,7 @@ import {
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import CustomPagination from "../layout/custom-pagination";
|
import CustomPagination from "../layout/custom-pagination";
|
||||||
|
import DatePicker from "react-datepicker";
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: "No", uid: "no" },
|
{ name: "No", uid: "no" },
|
||||||
|
|
@ -53,17 +54,18 @@ const columns = [
|
||||||
{ name: "Banner", uid: "isBanner" },
|
{ name: "Banner", uid: "isBanner" },
|
||||||
{ name: "Kategori", uid: "category" },
|
{ name: "Kategori", uid: "category" },
|
||||||
{ name: "Tanggal Unggah", uid: "createdAt" },
|
{ name: "Tanggal Unggah", uid: "createdAt" },
|
||||||
{ name: "Kreator", uid: "createdByName" },
|
{ name: "Kreator", uid: "customCreatorName" },
|
||||||
{ name: "Status", uid: "isPublish" },
|
{ name: "Status", uid: "isPublish" },
|
||||||
{ name: "Aksi", uid: "actions" },
|
{ name: "Aksi", uid: "actions" },
|
||||||
];
|
];
|
||||||
const columnsOtherRole = [
|
const columnsOtherRole = [
|
||||||
{ name: "No", uid: "no" },
|
{ name: "No", uid: "no" },
|
||||||
{ name: "Judul", uid: "title" },
|
{ name: "Judul", uid: "title" },
|
||||||
|
{ name: "Source", uid: "source" },
|
||||||
{ name: "Kategori", uid: "category" },
|
{ name: "Kategori", uid: "category" },
|
||||||
{ name: "Tanggal Unggah", uid: "createdAt" },
|
{ name: "Tanggal Unggah", uid: "createdAt" },
|
||||||
{ name: "Kreator", uid: "createdByName" },
|
{ name: "Kreator", uid: "customCreatorName" },
|
||||||
{ name: "Status", uid: "isPublish" },
|
{ name: "Status", uid: "publishStatus" },
|
||||||
{ name: "Aksi", uid: "actions" },
|
{ name: "Aksi", uid: "actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -84,7 +86,10 @@ export default function ArticleTable() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [categories, setCategories] = useState<any>([]);
|
const [categories, setCategories] = useState<any>([]);
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [selectedCategoryId, setSelectedCategoryId] = useState<any>("");
|
||||||
|
const [selectedSource, setSelectedSource] = useState<any>("");
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||||
|
const [dateRange, setDateRange] = useState<any>({
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
|
|
@ -98,24 +103,45 @@ export default function ArticleTable() {
|
||||||
const res = await getArticleByCategory();
|
const res = await getArticleByCategory();
|
||||||
const data = res?.data?.data;
|
const data = res?.data?.data;
|
||||||
setCategories(data);
|
setCategories(data);
|
||||||
|
console.log("category", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, [
|
||||||
|
page,
|
||||||
|
showData,
|
||||||
|
search,
|
||||||
|
selectedCategoryId,
|
||||||
|
selectedSource,
|
||||||
|
dateRange,
|
||||||
|
selectedStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
async function initState() {
|
async function initState() {
|
||||||
loading();
|
loading();
|
||||||
const req = {
|
const req = {
|
||||||
limit: showData,
|
limit: showData,
|
||||||
page: page,
|
page: page,
|
||||||
search: search,
|
search: search,
|
||||||
categorySlug: Array.from(selectedCategories).join(","),
|
category: selectedCategoryId || "",
|
||||||
|
source: selectedSource || "",
|
||||||
|
isPublish:
|
||||||
|
selectedStatus !== "" ? selectedStatus === "publish" : undefined,
|
||||||
|
startDate: formatDate(dateRange.startDate),
|
||||||
|
endDate: formatDate(dateRange.endDate),
|
||||||
sort: "desc",
|
sort: "desc",
|
||||||
sortBy: "created_at",
|
sortBy: "created_at",
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await getArticlePagination(req);
|
const res = await getArticlePagination(req);
|
||||||
await getTableNumber(parseInt(showData), res.data?.data);
|
|
||||||
|
let data = res.data?.data || [];
|
||||||
|
|
||||||
|
await getTableNumber(parseInt(showData), data);
|
||||||
setTotalPage(res?.data?.meta?.totalPage);
|
setTotalPage(res?.data?.meta?.totalPage);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// panggil ulang setiap state berubah
|
// panggil ulang setiap state berubah
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initState();
|
initState();
|
||||||
|
|
@ -173,11 +199,11 @@ export default function ArticleTable() {
|
||||||
initState();
|
initState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyUrlArticle = async (id: number, slug: string) => {
|
const copyUrlArticle = async (slug: any) => {
|
||||||
const url =
|
const url =
|
||||||
`${window.location.protocol}//${window.location.host}` +
|
`${window.location.protocol}//${window.location.host}` +
|
||||||
"/news/detail/" +
|
"/details/" +
|
||||||
`${id}-${slug}`;
|
`${slug}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
successToast("Success", "Article Copy to Clipboard");
|
successToast("Success", "Article Copy to Clipboard");
|
||||||
|
|
@ -192,7 +218,16 @@ export default function ArticleTable() {
|
||||||
const cellValue = article[columnKey as keyof any];
|
const cellValue = article[columnKey as keyof any];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "isPublish":
|
case "customCreatorName":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{article.customCreatorName &&
|
||||||
|
article.customCreatorName.trim() !== ""
|
||||||
|
? article.customCreatorName
|
||||||
|
: article.createdByName}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
case "publishStatus":
|
||||||
return (
|
return (
|
||||||
// <Chip
|
// <Chip
|
||||||
// className="capitalize "
|
// className="capitalize "
|
||||||
|
|
@ -204,7 +239,7 @@ export default function ArticleTable() {
|
||||||
// {article.status}
|
// {article.status}
|
||||||
// </div>
|
// </div>
|
||||||
// </Chip>
|
// </Chip>
|
||||||
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
<p>{article.publishStatus}</p>
|
||||||
);
|
);
|
||||||
case "isBanner":
|
case "isBanner":
|
||||||
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
||||||
|
|
@ -229,7 +264,7 @@ export default function ArticleTable() {
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => copyUrlArticle(article.id, article.slug)}
|
onClick={() => copyUrlArticle(article.slug)}
|
||||||
>
|
>
|
||||||
<CopyIcon className="mr-2 h-4 w-4" />
|
<CopyIcon className="mr-2 h-4 w-4" />
|
||||||
Copy Url Article
|
Copy Url Article
|
||||||
|
|
@ -245,8 +280,6 @@ 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}`}
|
||||||
|
|
@ -256,7 +289,6 @@ export default function ArticleTable() {
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
|
||||||
|
|
||||||
{username === "admin-mabes" && (
|
{username === "admin-mabes" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|
@ -271,13 +303,10 @@ 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>
|
||||||
|
|
@ -287,7 +316,7 @@ export default function ArticleTable() {
|
||||||
return cellValue;
|
return cellValue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[article, page]
|
[article, page],
|
||||||
);
|
);
|
||||||
|
|
||||||
let typingTimer: NodeJS.Timeout;
|
let typingTimer: NodeJS.Timeout;
|
||||||
|
|
@ -346,36 +375,78 @@ export default function ArticleTable() {
|
||||||
<div className="flex flex-col gap-1 w-full lg:w-[230px]">
|
<div className="flex flex-col gap-1 w-full lg:w-[230px]">
|
||||||
<p className="font-semibold text-sm">Kategori</p>
|
<p className="font-semibold text-sm">Kategori</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedCategories}
|
value={selectedCategoryId}
|
||||||
onValueChange={(value) => setSelectedCategories(value)}
|
onValueChange={(value) => setSelectedCategoryId(value)} // simpan ID
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full text-sm border">
|
<SelectTrigger className="w-full text-sm border">
|
||||||
<SelectValue placeholder="Kategori" />
|
<SelectValue placeholder="Kategori" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories
|
{categories
|
||||||
?.filter((category: any) => category.slug != null)
|
?.filter((category: any) => category.title != null)
|
||||||
.map((category: any) => (
|
.map((category: any) => (
|
||||||
<SelectItem key={category.slug} value={category.slug}>
|
<SelectItem
|
||||||
|
key={category.id}
|
||||||
|
value={category.id.toString()} // kirim ID, bukan title
|
||||||
|
>
|
||||||
{category.title}
|
{category.title}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
<div className="flex flex-col gap-1 w-full lg:w-[150px]">
|
||||||
|
<p className="font-semibold text-sm">Source</p>
|
||||||
|
<Select
|
||||||
|
value={selectedSource}
|
||||||
|
onValueChange={(value) => setSelectedSource(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full text-sm border">
|
||||||
|
<SelectValue placeholder="Pilih Source" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="internal">INTERNAL</SelectItem>
|
||||||
|
<SelectItem value="external">EXTERNAL</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 w-full lg:w-[150px]">
|
||||||
|
<p className="font-semibold text-sm">Status</p>
|
||||||
|
<Select
|
||||||
|
value={selectedStatus}
|
||||||
|
onValueChange={(value) => setSelectedStatus(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full text-sm border">
|
||||||
|
<SelectValue placeholder="Pilih Status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="publish">PUBLISH</SelectItem>
|
||||||
|
<SelectItem value="draft">DRAFT</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
||||||
<p className="font-semibold text-sm">Tanggal</p>
|
<p className="font-semibold text-sm">Tanggal</p>
|
||||||
<Datepicker
|
<DatePicker
|
||||||
value={startDateValue}
|
selectsRange
|
||||||
displayFormat="DD/MM/YYYY"
|
startDate={dateRange.startDate}
|
||||||
onChange={(e: any) => setStartDateValue(e)}
|
endDate={dateRange.endDate}
|
||||||
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
onChange={(update: [Date | null, Date | null]) => {
|
||||||
|
setDateRange({
|
||||||
|
startDate: update[0],
|
||||||
|
endDate: update[1],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isClearable
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
className="z-50 w-full text-sm bg-transparent border border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
||||||
|
placeholderText="Pilih rentang tanggal"
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-x-hidden">
|
<div className="w-full overflow-x-hidden">
|
||||||
<div className="w-full mx-auto overflow-x-hidden">
|
<div className="w-full overflow-x-auto">
|
||||||
<Table className="w-full table-fixed border text-sm">
|
<Table className="min-w-[1000px] w-full table-auto border text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{(username === "admin-mabes"
|
{(username === "admin-mabes"
|
||||||
|
|
@ -384,7 +455,18 @@ export default function ArticleTable() {
|
||||||
).map((column) => (
|
).map((column) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
className={`bg-white dark:bg-black text-black dark:text-white
|
||||||
|
text-sm font-semibold border-b px-3 py-3
|
||||||
|
${
|
||||||
|
column.uid === "no"
|
||||||
|
? "min-w-[60px] text-center"
|
||||||
|
: column.uid === "title"
|
||||||
|
? "min-w-[280px]"
|
||||||
|
: column.uid === "actions"
|
||||||
|
? "min-w-[100px] text-center"
|
||||||
|
: "min-w-[160px]"
|
||||||
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
@ -401,7 +483,17 @@ export default function ArticleTable() {
|
||||||
).map((column) => (
|
).map((column) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
className="truncate text-black dark:text-white max-w-[200px]"
|
className={`text-black dark:text-white text-sm px-3 py-3 align-top
|
||||||
|
${
|
||||||
|
column.uid === "no"
|
||||||
|
? "min-w-[60px] text-center font-medium"
|
||||||
|
: column.uid === "title"
|
||||||
|
? "min-w-[280px] whitespace-normal break-words leading-snug"
|
||||||
|
: column.uid === "actions"
|
||||||
|
? "min-w-[100px] text-center"
|
||||||
|
: "min-w-[160px] whitespace-normal break-words"
|
||||||
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{renderCell(item, column.uid)}
|
{renderCell(item, column.uid)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -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,15 +1,13 @@
|
||||||
import type { NextConfig } from "next";
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
images: {
|
images: {
|
||||||
domains: ["mikulnews.com", "dev.mikulnews.com"],
|
domains: ["mikulnews.com", "dev.mikulnews.com"],
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
// Add experimental features for better chunk handling
|
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['@ckeditor/ckeditor5-react', 'react-apexcharts'],
|
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -27,6 +27,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tinymce/tinymce-react": "^6.2.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"apexcharts": "^4.7.0",
|
"apexcharts": "^4.7.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
|
@ -38,12 +39,12 @@
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lightningcss": "^1.30.1",
|
"lightningcss": "^1.30.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.3.4",
|
"next": "^16.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.3",
|
||||||
"react-apexcharts": "^1.7.0",
|
"react-apexcharts": "^1.7.0",
|
||||||
"react-datepicker": "^8.4.0",
|
"react-datepicker": "^8.4.0",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.7.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.4",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.59.0",
|
||||||
"react-password-checklist": "^1.8.1",
|
"react-password-checklist": "^1.8.1",
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -1,6 +1,11 @@
|
||||||
import { PaginationRequest } from "@/types/globals";
|
import { PaginationRequest } from "@/types/globals";
|
||||||
import { httpGet } from "./http-config/http-base-services";
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
export async function getListArticle(props: PaginationRequest) {
|
export async function getListArticle(props: PaginationRequest) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -16,13 +21,14 @@ export async function getListArticle(props: PaginationRequest) {
|
||||||
categorySlug,
|
categorySlug,
|
||||||
isBanner,
|
isBanner,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return await httpGet(
|
return await httpGet(
|
||||||
`/articles?limit=${limit}&page=${page}&isPublish=${
|
`/articles?limit=${limit}&page=${page}&isPublish=${
|
||||||
isPublish === undefined ? "" : isPublish
|
isPublish === undefined ? "" : isPublish
|
||||||
}&title=${search}&startDate=${startDate || ""}&endDate=${
|
}&title=${search}&startDate=${startDate || ""}&endDate=${
|
||||||
endDate || ""
|
endDate || ""
|
||||||
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
||||||
sort || "asc"
|
sort || "desc"
|
||||||
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -40,13 +46,20 @@ export async function getArticlePagination(props: PaginationRequest) {
|
||||||
sort,
|
sort,
|
||||||
categorySlug,
|
categorySlug,
|
||||||
isBanner,
|
isBanner,
|
||||||
|
isPublish,
|
||||||
|
source,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return await httpGetInterceptor(
|
return await httpGetInterceptor(
|
||||||
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${startDate || ""}&endDate=${
|
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
|
||||||
endDate || ""
|
startDate || ""
|
||||||
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
}&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
|
||||||
sort || "asc"
|
source || ""
|
||||||
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`
|
}&isPublish=${isPublish !== undefined ? isPublish : ""}&sortBy=${
|
||||||
|
sortBy || "created_at"
|
||||||
|
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
|
||||||
|
isBanner || ""
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +94,11 @@ export async function updateArticle(id: string, data: any) {
|
||||||
return await httpPutInterceptor(pathUrl, data);
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function unPublishArticle(id: string, data: any) {
|
||||||
|
const pathUrl = `/articles/${id}/unpublish`;
|
||||||
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getArticleById(id: any) {
|
export async function getArticleById(id: any) {
|
||||||
const headers = {
|
const headers = {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
|
@ -88,6 +106,13 @@ export async function getArticleById(id: any) {
|
||||||
return await httpGet(`/articles/${id}`, headers);
|
return await httpGet(`/articles/${id}`, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getArticleBySlug(slug: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/articles/slug/${slug}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteArticle(id: string) {
|
export async function deleteArticle(id: string) {
|
||||||
const headers = {
|
const headers = {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
|
@ -111,6 +136,13 @@ export async function uploadArticleFile(id: string, data: any) {
|
||||||
return await httpPostInterceptor(`/article-files/${id}`, data, headers);
|
return await httpPostInterceptor(`/article-files/${id}`, data, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getArticleFiles() {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/article-files`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadArticleThumbnail(id: string, data: any) {
|
export async function uploadArticleThumbnail(id: string, data: any) {
|
||||||
const headers = {
|
const headers = {
|
||||||
"content-type": "multipart/form-data",
|
"content-type": "multipart/form-data",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const baseURL = "https://dev.mikulnews.com/api";
|
const baseURL = "https://mikulnews.com/api";
|
||||||
|
|
||||||
const axiosBaseInstance = axios.create({
|
const axiosBaseInstance = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
|
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import axios from "axios";
|
||||||
import { postSignIn } from "../master-user";
|
import { postSignIn } from "../master-user";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
const baseURL = "https://dev.mikulnews.com/api";
|
const baseURL = "https://mikulnews.com/api";
|
||||||
|
|
||||||
const refreshToken = Cookies.get("refresh_token");
|
const refreshToken = Cookies.get("refresh_token");
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ const axiosInterceptorInstance = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
|
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
|
||||||
},
|
},
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
@ -28,7 +28,7 @@ axiosInterceptorInstance.interceptors.request.use(
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor
|
// Response interceptor
|
||||||
|
|
@ -66,7 +66,7 @@ axiosInterceptorInstance.interceptors.response.use(
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default axiosInterceptorInstance;
|
export default axiosInterceptorInstance;
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,21 @@ export async function otpValidation(email: string, otpCode: string) {
|
||||||
return await httpPost(pathUrl, { email, otpCode });
|
return await httpPost(pathUrl, { email, otpCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export async function postArticleComment(data: any) {
|
||||||
|
// const headers = token
|
||||||
|
// ? {
|
||||||
|
// "content-type": "application/json",
|
||||||
|
// Authorization: `${token}`,
|
||||||
|
// }
|
||||||
|
// : {
|
||||||
|
// "content-type": "application/json",
|
||||||
|
// };
|
||||||
|
// return await httpPost(`/article-comments`, headers, data);
|
||||||
|
// }
|
||||||
|
|
||||||
export async function postArticleComment(data: any) {
|
export async function postArticleComment(data: any) {
|
||||||
const headers = token
|
const pathUrl = `/article-comments`;
|
||||||
? {
|
return await httpPostInterceptor(pathUrl, data);
|
||||||
"content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
};
|
|
||||||
return await httpPost(`/article-comments`, headers, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editArticleComment(data: any, id: number) {
|
export async function editArticleComment(data: any, id: number) {
|
||||||
|
|
@ -112,7 +117,7 @@ export async function editArticleComment(data: any, id: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArticleComment(id: string) {
|
export async function getArticleComment(id: string) {
|
||||||
const pathUrl = `/article-comments?isPublic=true&articleId=${id}`;
|
const pathUrl = `/article-comments?isPublic=false&articleId=${id}`;
|
||||||
return await httpGet(pathUrl);
|
return await httpGet(pathUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/* ========== CKEditor Wrapper ========== */
|
||||||
|
.ckeditor-wrapper {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Main Editor Container ========== */
|
||||||
|
.ckeditor-wrapper .ck.ck-editor__main {
|
||||||
|
min-height: var(--editor-min-height, 400px);
|
||||||
|
max-height: var(--editor-max-height, 600px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Editable Content Area (ClassicEditor) ========== */
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline {
|
||||||
|
min-height: calc(var(--editor-min-height, 400px) - 50px);
|
||||||
|
max-height: calc(var(--editor-max-height, 600px) - 50px);
|
||||||
|
overflow-y: auto !important;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #111 !important;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 1rem;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Headings and Text Formatting ========== */
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h1,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h2,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h3,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h4,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h5,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline p {
|
||||||
|
margin: 0.5em 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline ul,
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 4px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Dark Mode Support ========== */
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline {
|
||||||
|
background: #111 !important;
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h1,
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h2,
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h3,
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h4,
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h5,
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline h6 {
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline blockquote {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
border-left-color: #374151 !important;
|
||||||
|
color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Custom Scrollbars (Light & Dark) ========== */
|
||||||
|
.ckeditor-wrapper .ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-track {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark
|
||||||
|
.ckeditor-wrapper
|
||||||
|
.ck.ck-content.ck-editor__editable_inline::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
@ -11,7 +15,7 @@
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|
@ -19,9 +23,19 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,7 @@ export type PaginationRequest = {
|
||||||
category?: string;
|
category?: string;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
|
source?: string;
|
||||||
categorySlug?: string;
|
categorySlug?: string;
|
||||||
isBanner?: boolean;
|
isBanner?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -164,3 +164,11 @@ export function convertDateFormatNoTime(date: Date): string {
|
||||||
const day = `${date.getDate()}`.padStart(2, "0");
|
const day = `${date.getDate()}`.padStart(2, "0");
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date | null) {
|
||||||
|
if (!date) return "";
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,67 @@
|
||||||
* @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
*/
|
*/
|
||||||
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
|
import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
|
||||||
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
import { Alignment } from "@ckeditor/ckeditor5-alignment";
|
||||||
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
import { Autoformat } from "@ckeditor/ckeditor5-autoformat";
|
||||||
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
|
import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles";
|
||||||
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
import { BlockQuote } from "@ckeditor/ckeditor5-block-quote";
|
||||||
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
|
import { CloudServices } from "@ckeditor/ckeditor5-cloud-services";
|
||||||
import { CodeBlock } from '@ckeditor/ckeditor5-code-block';
|
import { CodeBlock } from "@ckeditor/ckeditor5-code-block";
|
||||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core';
|
import type { EditorConfig } from "@ckeditor/ckeditor5-core";
|
||||||
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
import { Essentials } from "@ckeditor/ckeditor5-essentials";
|
||||||
import { FontSize } from '@ckeditor/ckeditor5-font';
|
import { FontSize } from "@ckeditor/ckeditor5-font";
|
||||||
import { Heading } from '@ckeditor/ckeditor5-heading';
|
import { Heading } from "@ckeditor/ckeditor5-heading";
|
||||||
import { Image, ImageCaption, ImageInsert, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image';
|
import {
|
||||||
import { Indent } from '@ckeditor/ckeditor5-indent';
|
Image,
|
||||||
import { Link } from '@ckeditor/ckeditor5-link';
|
ImageCaption,
|
||||||
import { List } from '@ckeditor/ckeditor5-list';
|
ImageInsert,
|
||||||
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
ImageStyle,
|
||||||
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
ImageToolbar,
|
||||||
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
ImageUpload,
|
||||||
import { SourceEditing } from '@ckeditor/ckeditor5-source-editing';
|
} from "@ckeditor/ckeditor5-image";
|
||||||
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
import { Indent } from "@ckeditor/ckeditor5-indent";
|
||||||
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
import { Link } from "@ckeditor/ckeditor5-link";
|
||||||
import { Undo } from '@ckeditor/ckeditor5-undo';
|
import { List } from "@ckeditor/ckeditor5-list";
|
||||||
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';
|
import { MediaEmbed } from "@ckeditor/ckeditor5-media-embed";
|
||||||
|
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
|
||||||
|
import { PasteFromOffice } from "@ckeditor/ckeditor5-paste-from-office";
|
||||||
|
import { SourceEditing } from "@ckeditor/ckeditor5-source-editing";
|
||||||
|
import { Table, TableToolbar } from "@ckeditor/ckeditor5-table";
|
||||||
|
import { TextTransformation } from "@ckeditor/ckeditor5-typing";
|
||||||
|
import { Undo } from "@ckeditor/ckeditor5-undo";
|
||||||
|
import { SimpleUploadAdapter } from "@ckeditor/ckeditor5-upload";
|
||||||
declare class Editor extends ClassicEditor {
|
declare class Editor extends ClassicEditor {
|
||||||
static builtinPlugins: (typeof Alignment | typeof Autoformat | typeof BlockQuote | typeof Bold | typeof CloudServices | typeof CodeBlock | typeof Essentials | typeof FontSize | typeof Heading | typeof Image | typeof ImageCaption | typeof ImageInsert | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof Indent | typeof Italic | typeof Link | typeof List | typeof MediaEmbed | typeof Paragraph | typeof PasteFromOffice | typeof SimpleUploadAdapter | typeof SourceEditing | typeof Table | typeof TableToolbar | typeof TextTransformation | typeof Undo)[];
|
static builtinPlugins: (
|
||||||
|
| 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
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/cs/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/de/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/de/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/es/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/es/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/fr/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/fr/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/it/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/it/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ja/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ja/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ko/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ko/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pl/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pl/diagnosticMessages.generated.json
generated
vendored
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
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/pt-br/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ru/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/ru/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/tr/diagnosticMessages.generated.json
generated
vendored
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/tr/diagnosticMessages.generated.json
generated
vendored
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
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-cn/diagnosticMessages.generated.json
generated
vendored
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
1880
vendor/ckeditor5/node_modules/.typescript-7i6ZGcGl/lib/zh-tw/diagnosticMessages.generated.json
generated
vendored
File diff suppressed because it is too large
Load Diff
162
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/CHANGELOG.md
generated
vendored
Normal file
162
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/CHANGELOG.md
generated
vendored
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
All changes in the package are documented in the main repository. See: https://github.com/ckeditor/ckeditor5/blob/master/CHANGELOG.md.
|
||||||
|
|
||||||
|
Changes for the past releases are available below.
|
||||||
|
|
||||||
|
## [19.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v18.0.0...v19.0.0) (April 29, 2020)
|
||||||
|
|
||||||
|
Internal changes only (updated dependencies, documentation, etc.).
|
||||||
|
|
||||||
|
|
||||||
|
## [18.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v17.0.0...v18.0.0) (March 19, 2020)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([f1beaaa](https://github.com/ckeditor/ckeditor5-alignment/commit/f1beaaa))
|
||||||
|
|
||||||
|
|
||||||
|
## [17.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v16.0.0...v17.0.0) (February 18, 2020)
|
||||||
|
|
||||||
|
### MAJOR BREAKING CHANGES
|
||||||
|
|
||||||
|
* The `align-left`, `align-right`, `align-center`, and `align-justify` icons have been moved to `@ckeditor/ckeditor5-core`.
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Moved alignment icons to `@ckeditor/ckeditor5-core` (see [ckeditor/ckeditor5-table#227](https://github.com/ckeditor/ckeditor5-table/issues/227)). ([410e279](https://github.com/ckeditor/ckeditor5-alignment/commit/410e279))
|
||||||
|
* Updated translations. ([288672f](https://github.com/ckeditor/ckeditor5-alignment/commit/288672f))
|
||||||
|
|
||||||
|
|
||||||
|
## [16.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v15.0.0...v16.0.0) (December 4, 2019)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([9085f7b](https://github.com/ckeditor/ckeditor5-alignment/commit/9085f7b))
|
||||||
|
|
||||||
|
|
||||||
|
## [15.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.2.0...v15.0.0) (October 23, 2019)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([a719974](https://github.com/ckeditor/ckeditor5-alignment/commit/a719974)) ([2fed077](https://github.com/ckeditor/ckeditor5-alignment/commit/2fed077))
|
||||||
|
* Added `pluginName` to the editor plugin part of the feature. ([3b42798](https://github.com/ckeditor/ckeditor5-alignment/commit/3b42798))
|
||||||
|
|
||||||
|
|
||||||
|
## [11.2.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.3...v11.2.0) (August 26, 2019)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Integrated the text alignment feature with different editor content directions (LTR and RTL). See [ckeditor/ckeditor5#1151](https://github.com/ckeditor/ckeditor5/issues/1151). ([edc7d8b](https://github.com/ckeditor/ckeditor5-alignment/commit/edc7d8b))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* The UI buttons should be marked as toggleable for better assistive technologies support (see [ckeditor/ckeditor5#1403](https://github.com/ckeditor/ckeditor5/issues/1403)). ([599ea01](https://github.com/ckeditor/ckeditor5-alignment/commit/599ea01))
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([54f81b3](https://github.com/ckeditor/ckeditor5-alignment/commit/54f81b3))
|
||||||
|
* The text alignment toolbar should have a proper `aria-label` attribute (see [ckeditor/ckeditor5#1404](https://github.com/ckeditor/ckeditor5/issues/1404)). ([3ed81de](https://github.com/ckeditor/ckeditor5-alignment/commit/3ed81de))
|
||||||
|
* Updated translations. ([feb4ab3](https://github.com/ckeditor/ckeditor5-alignment/commit/feb4ab3))
|
||||||
|
|
||||||
|
|
||||||
|
## [11.1.3](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.2...v11.1.3) (July 10, 2019)
|
||||||
|
|
||||||
|
Internal changes only (updated dependencies, documentation, etc.).
|
||||||
|
|
||||||
|
|
||||||
|
## [11.1.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.1...v11.1.2) (July 4, 2019)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([bb7f494](https://github.com/ckeditor/ckeditor5-alignment/commit/bb7f494))
|
||||||
|
|
||||||
|
|
||||||
|
## [11.1.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.1.0...v11.1.1) (June 6, 2019)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([32c32c1](https://github.com/ckeditor/ckeditor5-alignment/commit/32c32c1))
|
||||||
|
|
||||||
|
|
||||||
|
## [11.1.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v11.0.0...v11.1.0) (April 4, 2019)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Marked alignment as a formatting attribute using the `AttributeProperties#isFormatting` property. Closes [ckeditor/ckeditor5#1664](https://github.com/ckeditor/ckeditor5/issues/1664). ([6358e08](https://github.com/ckeditor/ckeditor5-alignment/commit/6358e08))
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([78bfc40](https://github.com/ckeditor/ckeditor5-alignment/commit/78bfc40))
|
||||||
|
|
||||||
|
|
||||||
|
## [11.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.4...v11.0.0) (February 28, 2019)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([45e8dd5](https://github.com/ckeditor/ckeditor5-alignment/commit/45e8dd5)) ([a92c37b](https://github.com/ckeditor/ckeditor5-alignment/commit/a92c37b)) ([ef68e54](https://github.com/ckeditor/ckeditor5-alignment/commit/ef68e54))
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* Upgraded minimal versions of Node to `8.0.0` and npm to `5.7.1`. See: [ckeditor/ckeditor5#1507](https://github.com/ckeditor/ckeditor5/issues/1507). ([612ea3c](https://github.com/ckeditor/ckeditor5-cloud-services/commit/612ea3c))
|
||||||
|
|
||||||
|
|
||||||
|
## [10.0.4](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.3...v10.0.4) (December 5, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Improved SVG icons size. See [ckeditor/ckeditor5-theme-lark#206](https://github.com/ckeditor/ckeditor5-theme-lark/issues/206). ([1d71d33](https://github.com/ckeditor/ckeditor5-alignment/commit/1d71d33))
|
||||||
|
* Updated translations. ([547f8d8](https://github.com/ckeditor/ckeditor5-alignment/commit/547f8d8)) ([43d8225](https://github.com/ckeditor/ckeditor5-alignment/commit/43d8225))
|
||||||
|
|
||||||
|
|
||||||
|
## [10.0.3](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.2...v10.0.3) (October 8, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([5b30202](https://github.com/ckeditor/ckeditor5-alignment/commit/5b30202))
|
||||||
|
|
||||||
|
|
||||||
|
## [10.0.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.1...v10.0.2) (July 18, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([33c281c](https://github.com/ckeditor/ckeditor5-alignment/commit/33c281c))
|
||||||
|
|
||||||
|
|
||||||
|
## [10.0.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v10.0.0...v10.0.1) (June 21, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations.
|
||||||
|
|
||||||
|
|
||||||
|
## [10.0.0](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.4...v10.0.0) (April 25, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Changed the license to GPL2+ only. See [ckeditor/ckeditor5#991](https://github.com/ckeditor/ckeditor5/issues/991). ([eed1029](https://github.com/ckeditor/ckeditor5-alignment/commit/eed1029))
|
||||||
|
* Updated translations. ([baa1fbe](https://github.com/ckeditor/ckeditor5-alignment/commit/baa1fbe))
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* The license under which CKEditor 5 is released has been changed from a triple GPL, LGPL and MPL license to a GPL2+ only. See [ckeditor/ckeditor5#991](https://github.com/ckeditor/ckeditor5/issues/991) for more information.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-beta.4](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.2...v1.0.0-beta.4) (April 19, 2018)
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Updated translations. ([586ae62](https://github.com/ckeditor/ckeditor5-alignment/commit/586ae62))
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-beta.2](https://github.com/ckeditor/ckeditor5-alignment/compare/v1.0.0-beta.1...v1.0.0-beta.2) (April 10, 2018)
|
||||||
|
|
||||||
|
Internal changes only (updated dependencies, documentation, etc.).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-alignment/compare/v0.0.1...v1.0.0-beta.1) (March 15, 2018)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Initial implementation. Closes [#2](https://github.com/ckeditor/ckeditor5-alignment/issues/2).
|
||||||
17
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/LICENSE.md
generated
vendored
Normal file
17
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/LICENSE.md
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
Software License Agreement
|
||||||
|
==========================
|
||||||
|
|
||||||
|
**CKEditor 5 text alignment feature** – https://github.com/ckeditor/ckeditor5-alignment <br>
|
||||||
|
Copyright (c) 2003–2024, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
|
||||||
|
|
||||||
|
Sources of Intellectual Property Included in CKEditor
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
|
||||||
|
|
||||||
|
Trademarks
|
||||||
|
----------
|
||||||
|
|
||||||
|
**CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks, or service marks of their respective holders.
|
||||||
20
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/README.md
generated
vendored
Normal file
20
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
CKEditor 5 text alignment feature
|
||||||
|
========================================
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@ckeditor/ckeditor5-alignment)
|
||||||
|
[](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
|
||||||
|
[](https://app.travis-ci.com/github/ckeditor/ckeditor5)
|
||||||
|
|
||||||
|
This package implements text alignment support for CKEditor 5.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Check out the [demo in the text alignment feature guide](https://ckeditor.com/docs/ckeditor5/latest/features/text-alignment.html#demo).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See the [`@ckeditor/ckeditor5-alignment` package](https://ckeditor.com/docs/ckeditor5/latest/api/alignment.html) page in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).
|
||||||
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/alignment.js
generated
vendored
Normal file
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/alignment.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/af.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/af.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const e=n.af=n.af||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs",Justify:"Belyn beide kante","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ar.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ar.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.ar=n.ar||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"محاذاة في المنتصف","Align left":"محاذاة لليسار","Align right":"محاذاة لليمين",Justify:"ضبط","Text alignment":"محاذاة النص","Text alignment toolbar":"شريط أدوات محاذاة النص"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/az.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/az.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.az=n.az||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Mərkəzə düzləndir","Align left":"Soldan düzləndir","Align right":"Sağdan düzləndir",Justify:"Eninə görə","Text alignment":"Mətn düzləndirməsi","Text alignment toolbar":"Mətnin düzləndirmə paneli"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bg.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bg.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.bg=n.bg||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Централно подравняване","Align left":"Ляво подравняване","Align right":"Дясно подравняване",Justify:"Разпредели по равно","Text alignment":"Подравняване на текста","Text alignment toolbar":"Лента за подравняване на текст"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bn.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bn.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.bn=n.bn||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"কেন্দ্র সারিবদ্ধ করুন","Align left":"বামে সারিবদ্ধ করুন","Align right":"ডানদিকে সারিবদ্ধ করুন",Justify:"জাস্টিফাই","Text alignment":"টেক্সট সারিবদ্ধকরণ","Text alignment toolbar":"টেক্সট শ্রেণীবিন্যাস টুলবার"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bs.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/bs.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const a=n.bs=n.bs||{};a.dictionary=Object.assign(a.dictionary||{},{"Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Justify:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ca.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ca.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(i){const e=i.ca=i.ca||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Alineació centre","Align left":"Alineació esquerra","Align right":"Alineació dreta",Justify:"Justificar","Text alignment":"Alineació text","Text alignment toolbar":"Barra d'eines d'alineació de text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/cs.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/cs.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const t=n.cs=n.cs||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Zarovnat na střed","Align left":"Zarovnat vlevo","Align right":"Zarovnat vpravo",Justify:"Zarovnat do bloku","Text alignment":"Zarovnání textu","Text alignment toolbar":"Panel nástrojů zarovnání textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/da.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/da.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(t){const n=t.da=t.da||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Justér center","Align left":"Justér venstre","Align right":"Justér højre",Justify:"Justér","Text alignment":"Tekstjustering","Text alignment toolbar":"Tekstjustering værktøjslinje"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/de-ch.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/de-ch.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(t){const i=t["de-ch"]=t["de-ch"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Zentriert","Align left":"Linksbündig","Align right":"Rechtsbündig",Justify:"Blocksatz","Text alignment":"Textausrichtung","Text alignment toolbar":"Textausrichtung Werkzeugleiste"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/de.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/de.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const t=n.de=n.de||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Zentriert","Align left":"Linksbündig","Align right":"Rechtsbündig",Justify:"Blocksatz","Text alignment":"Textausrichtung","Text alignment toolbar":"Text-Ausrichtung Toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/el.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/el.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.el=n.el||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Στοίχιση στο κέντρο","Align left":"Στοίχιση αριστερά","Align right":"Στοίχιση δεξιά",Justify:"Πλήρης στοίχηση","Text alignment":"Στοίχιση κειμένου","Text alignment toolbar":"Γραμμή εργαλείων στοίχισης κειμένου"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/en-au.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/en-au.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const t=n["en-au"]=n["en-au"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Align centre","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":"Text alignment toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/en-gb.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/en-gb.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n["en-gb"]=n["en-gb"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Align center","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":""})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/es-co.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/es-co.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(i){const e=i["es-co"]=i["es-co"]||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Centrar","Align left":"Alinear a la izquierda","Align right":"Alinear a la derecha",Justify:"Justificar","Text alignment":"Alineación de texto","Text alignment toolbar":"Herramientas de alineación de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/es.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/es.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(e){const i=e.es=e.es||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Centrar","Align left":"Alinear a la izquierda","Align right":"Alinear a la derecha",Justify:"Justificar","Text alignment":"Alineación del texto","Text alignment toolbar":"Barra de herramientas de alineación del texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/et.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/et.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.et=n.et||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Keskjoondus","Align left":"Vasakjoondus","Align right":"Paremjoondus",Justify:"Rööpjoondus","Text alignment":"Teksti joondamine","Text alignment toolbar":"Teksti joonduse tööriistariba"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fa.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fa.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.fa=n.fa||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"تراز وسط","Align left":"تراز چپ","Align right":"تراز راست",Justify:"هم تراز کردن","Text alignment":"تراز متن","Text alignment toolbar":"نوار ابزار ترازبندی متن"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fi.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fi.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(a){const i=a.fi=a.fi||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Tasaa keskelle","Align left":"Tasaa vasemmalle","Align right":"Tasaa oikealle",Justify:"Tasaa molemmat reunat","Text alignment":"Tekstin tasaus","Text alignment toolbar":"Tekstin suuntauksen työkalupalkki"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fr.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/fr.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(e){const t=e.fr=e.fr||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Centrer","Align left":"Aligner à gauche","Align right":"Aligner à droite",Justify:"Justifier","Text alignment":"Alignement du texte","Text alignment toolbar":"Barre d'outils d'alignement du texte"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/gl.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/gl.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(t){const e=t.gl=t.gl||{};e.dictionary=Object.assign(e.dictionary||{},{"Align center":"Centrar horizontalmente","Align left":"Aliñar á esquerda","Align right":"Aliñar á dereita",Justify:"Xustificado","Text alignment":"Aliñamento do texto","Text alignment toolbar":"Barra de ferramentas de aliñamento de textos"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/he.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/he.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.he=n.he||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"יישור באמצע","Align left":"יישור לשמאל","Align right":"יישור לימין",Justify:"מרכוז גבולות","Text alignment":"יישור טקסט","Text alignment toolbar":"סרגל כלים יישור טקסט"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hi.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hi.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(i){const n=i.hi=i.hi||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Align center","Align left":"Align left","Align right":"Align right",Justify:"Justify","Text alignment":"Text alignment","Text alignment toolbar":"Text alignment toolbar"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hr.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hr.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const a=n.hr=n.hr||{};a.dictionary=Object.assign(a.dictionary||{},{"Align center":"Poravnaj po sredini","Align left":"Poravnaj ulijevo","Align right":"Poravnaj udesno",Justify:"Razvuci","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hu.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/hu.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(i){const t=i.hu=i.hu||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Középre igazítás","Align left":"Balra igazítás","Align right":"Jobbra igazítás",Justify:"Sorkizárt","Text alignment":"Szöveg igazítása","Text alignment toolbar":"Szöveg igazítás eszköztár"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/id.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/id.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(a){const t=a.id=a.id||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Rata tengah","Align left":"Rata kiri","Align right":"Rata kanan",Justify:"Rata kanan-kiri","Text alignment":"Perataan teks","Text alignment toolbar":"Alat perataan teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/it.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/it.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(i){const n=i.it=i.it||{};n.dictionary=Object.assign(n.dictionary||{},{"Align center":"Allinea al centro","Align left":"Allinea a sinistra","Align right":"Allinea a destra",Justify:"Giustifica","Text alignment":"Allineamento del testo","Text alignment toolbar":"Barra degli strumenti dell'allineamento"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ja.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/ja.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.ja=n.ja||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"中央揃え","Align left":"左揃え","Align right":"右揃え",Justify:"両端揃え","Text alignment":"文字揃え","Text alignment toolbar":"テキストの整列"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/jv.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/jv.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const t=n.jv=n.jv||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Rata tengah","Align left":"Rata kiwa","Align right":"Rata tengen",Justify:"Rata kiwa tengen","Text alignment":"Perataan seratan","Text alignment toolbar":""})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/kk.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/kk.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.kk=n.kk||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"Ортадан туралау","Align left":"Солға туралау","Align right":"Оңға туралау",Justify:"","Text alignment":"Мәтінді туралау","Text alignment toolbar":"Мәтінді туралау құралдар тақтасы"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/km.js
generated
vendored
Normal file
1
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-alignment/build/translations/km.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(n){const i=n.km=n.km||{};i.dictionary=Object.assign(i.dictionary||{},{"Align center":"តម្រឹមកណ្ដាល","Align left":"តម្រឹមឆ្វេង","Align right":"តម្រឹមស្ដាំ",Justify:"តម្រឹមសងខាង","Text alignment":"ការតម្រឹមអក្សរ","Text alignment toolbar":"របារឧបករណ៍តម្រឹមអក្សរ"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue