update
This commit is contained in:
parent
2987c890ec
commit
ccc708ac23
|
|
@ -1,14 +1,19 @@
|
||||||
|
import BannerNews from "@/components/landing-page/banner-news";
|
||||||
import Footer from "@/components/landing-page/footer";
|
import Footer from "@/components/landing-page/footer";
|
||||||
import HeaderPopular from "@/components/landing-page/headers-popular";
|
import HeaderLatest from "@/components/landing-page/headers-latest";
|
||||||
import Navbar from "@/components/landing-page/navbar";
|
import LatestNews from "@/components/landing-page/latest-news";
|
||||||
|
|
||||||
export default function PopularPage() {
|
import Navbar from "@/components/landing-page/navbar";
|
||||||
|
import News from "@/components/landing-page/news";
|
||||||
|
|
||||||
|
export default function LatestPage() {
|
||||||
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="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<HeaderPopular />
|
<BannerNews />
|
||||||
|
<LatestNews />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import DetailContent from "@/components/details/details-content";
|
||||||
|
import Footer from "@/components/landing-page/footer";
|
||||||
|
import Navbar from "@/components/landing-page/navbar";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||||
|
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex-1">
|
||||||
|
<DetailContent />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import Link from "next/link";
|
||||||
import { getArticleById, getListArticle } from "@/service/article";
|
import { getArticleById, getListArticle } from "@/service/article";
|
||||||
import { close, loading } from "@/config/swal";
|
import { close, loading } from "@/config/swal";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { CommentIcon } from "../icons/sidebar-icon";
|
|
||||||
|
|
||||||
type TabKey = "trending" | "comments" | "latest";
|
type TabKey = "trending" | "comments" | "latest";
|
||||||
|
|
||||||
|
|
@ -40,9 +39,9 @@ export default function DetailContent() {
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [articleDetail, setArticleDetail] = useState<any>(null);
|
const [articleDetail, setArticleDetail] = useState<any>(null);
|
||||||
const [showData, setShowData] = useState("5");
|
const [showData, setShowData] = useState("5");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("-");
|
||||||
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
|
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
|
||||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
const [selectedCategories, setSelectedCategories] = useState<any>("-");
|
||||||
const [startDateValue, setStartDateValue] = useState({
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
|
|
@ -56,6 +55,8 @@ export default function DetailContent() {
|
||||||
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");
|
||||||
|
|
@ -67,29 +68,46 @@ export default function DetailContent() {
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initState();
|
fetchTabArticles();
|
||||||
}, [page, showData, startDateValue, selectedCategories]);
|
}, [activeTab]);
|
||||||
|
|
||||||
async function initState() {
|
async function fetchTabArticles() {
|
||||||
// loading();
|
const req: any = {
|
||||||
const req = {
|
|
||||||
limit: showData,
|
limit: showData,
|
||||||
page,
|
page,
|
||||||
search,
|
search,
|
||||||
categorySlug: Array.from(selectedCategories).join(","),
|
categorySlug: Array.from(selectedCategories).join(","),
|
||||||
sort: "desc",
|
sort: "desc",
|
||||||
isPublish: true,
|
|
||||||
sortBy: "created_at",
|
sortBy: "created_at",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sesuaikan sortBy berdasarkan tab
|
||||||
|
if (activeTab === "trending") {
|
||||||
|
req.sortBy = "view_count";
|
||||||
|
} else if (activeTab === "comments") {
|
||||||
|
req.sortBy = "comment_count";
|
||||||
|
} else {
|
||||||
|
req.sortBy = "created_at";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hanya kirimkan search jika valid
|
||||||
|
if (search && search !== "-" && search !== "") {
|
||||||
|
req.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCategories && selectedCategories !== "-") {
|
||||||
|
req.categorySlug = selectedCategories;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getListArticle(req);
|
const res = await getListArticle(req);
|
||||||
setArticles(res?.data?.data || []);
|
setTabArticles(res?.data?.data || []);
|
||||||
setTotalPage(res?.data?.meta?.totalPage || 1);
|
} catch (error) {
|
||||||
} finally {
|
console.error("Failed fetching tab articles:", error);
|
||||||
// close();
|
setTabArticles([]); // Optional fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content: Record<
|
const content: Record<
|
||||||
TabKey,
|
TabKey,
|
||||||
{ image: string; title: string; date: string }[]
|
{ image: string; title: string; date: string }[]
|
||||||
|
|
@ -135,6 +153,31 @@ export default function DetailContent() {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, [page, showData]);
|
||||||
|
|
||||||
|
async function initState() {
|
||||||
|
// loading();
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page: 1,
|
||||||
|
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);
|
||||||
|
} finally {
|
||||||
|
// close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initStateData();
|
initStateData();
|
||||||
}, [listCategory]);
|
}, [listCategory]);
|
||||||
|
|
@ -147,10 +190,18 @@ export default function DetailContent() {
|
||||||
setThumbnail(data?.thumbnailUrl);
|
setThumbnail(data?.thumbnailUrl);
|
||||||
setDiseId(data?.aiArticleId);
|
setDiseId(data?.aiArticleId);
|
||||||
setDetailFiles(data?.files);
|
setDetailFiles(data?.files);
|
||||||
setArticleDetail(data); // <-- Add this
|
setArticleDetail(data);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!articleDetail?.files || articleDetail.files.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
|
||||||
|
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="bg-white grid grid-cols-1 md:grid-cols-3 gap-6 px-8 py-8">
|
<div className="bg-white grid grid-cols-1 md:grid-cols-3 gap-6 px-8 py-8">
|
||||||
|
|
@ -159,76 +210,89 @@ export default function DetailContent() {
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1a1a1a] leading-tight mb-4">
|
<h1 className="text-3xl md:text-4xl font-bold text-[#1a1a1a] leading-tight mb-4">
|
||||||
{articleDetail?.title}
|
{articleDetail?.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-row justify-between items-center space-x-2 text-sm text-black mb-4">
|
<div className="flex items-center space-x-2 text-sm text-gray-500 mb-4">
|
||||||
<div className="flex flex-row gap-3">
|
<div className="text-orange-400">
|
||||||
<div className="text-[#31942E]">
|
<svg
|
||||||
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
width="24"
|
||||||
width="24"
|
height="24"
|
||||||
height="24"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
// fill-rule="evenodd"
|
||||||
>
|
>
|
||||||
<g
|
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.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.003-.011l.018-.43l-.003-.012l-.01-.01z" />
|
||||||
fill="none"
|
<path
|
||||||
// fill-rule="evenodd"
|
fill="currentColor"
|
||||||
>
|
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984"
|
||||||
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.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.003-.011l.018-.43l-.003-.012l-.01-.01z" />
|
/>
|
||||||
<path
|
</g>
|
||||||
fill="currentColor"
|
</svg>
|
||||||
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984"
|
</div>
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-[#31942E] font-medium">
|
<span className="text-orange-400 font-medium">
|
||||||
{articleDetail?.createdByName}
|
{articleDetail?.createdByName}
|
||||||
</span>
|
</span>
|
||||||
<span>-</span>
|
<span>•</span>
|
||||||
|
<span>
|
||||||
<span>
|
<span>
|
||||||
<span>
|
{new Date(articleDetail?.createdAt).toLocaleDateString(
|
||||||
{new Date(articleDetail?.createdAt).toLocaleDateString(
|
"id-ID",
|
||||||
"id-ID",
|
{
|
||||||
{
|
day: "numeric",
|
||||||
day: "numeric",
|
month: "long",
|
||||||
month: "long",
|
year: "numeric",
|
||||||
year: "numeric",
|
}
|
||||||
}
|
)}
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-500">in</span>
|
</span>
|
||||||
<span>{articleDetail?.categories?.[0]?.title}</span>
|
<span>•</span>
|
||||||
</div>
|
<span>{articleDetail?.categories?.[0]?.title}</span>
|
||||||
<div className="flex items-center">
|
|
||||||
<CommentIcon />0
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-auto mb-6">
|
<div className="w-full h-auto mb-6">
|
||||||
{articleDetail?.files?.[0]?.fileUrl ? (
|
{/* Gambar utama */}
|
||||||
|
<div className="w-full">
|
||||||
<Image
|
<Image
|
||||||
src={articleDetail.files[0].fileUrl}
|
src={articleDetail.files[selectedIndex].fileUrl}
|
||||||
alt="Berita"
|
alt={articleDetail.files[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>
|
||||||
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
|
|
||||||
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className=" flex flex-row w-fit rounded overflow-hidden mr-5 gap-3">
|
{/* Thumbnail */}
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex gap-2 mt-3 overflow-x-auto">
|
||||||
<p className="text-red-500 font-semibold">0</p>
|
{articleDetail.files.map((file: any, index: number) => (
|
||||||
<p className="text-red-500 font-semibold">SHARES</p>
|
<button
|
||||||
</div>
|
key={file.id || index}
|
||||||
<div className="flex flex-col items-center gap-2">
|
onClick={() => setSelectedIndex(index)}
|
||||||
<p className="text-black font-semibold">3</p>
|
className={`border-2 rounded-lg overflow-hidden ${
|
||||||
<p className="text-black font-semibold">VIEWS</p>
|
selectedIndex === index
|
||||||
</div>
|
? "border-red-500"
|
||||||
|
: "border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={file.fileUrl}
|
||||||
|
alt={file.fileAlt || "Thumbnail"}
|
||||||
|
width={100}
|
||||||
|
height={80}
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slug */}
|
||||||
|
<p className="text-sm text-gray-500 mt-2 text-end">
|
||||||
|
{articleDetail?.slug}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex relative">
|
||||||
|
<div className=" flex flex-col w-fit rounded overflow-hidden mr-5">
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="#"
|
||||||
aria-label="Facebook"
|
aria-label="Facebook"
|
||||||
|
|
@ -263,89 +327,45 @@ export default function DetailContent() {
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="#"
|
||||||
aria-label="WhatsApp"
|
aria-label="Google"
|
||||||
className="bg-green-700 p-4 flex justify-center items-center text-white"
|
className="bg-[#fce9e7] p-4 flex justify-center items-center text-white"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g fill="none">
|
<path 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" />
|
||||||
<g
|
|
||||||
// clip-path="url(#SVGXv8lpc2Y)"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
// fill-rule="evenodd"
|
|
||||||
d="M17.415 14.382c-.298-.149-1.759-.867-2.031-.967s-.47-.148-.669.15c-.198.297-.767.966-.94 1.164c-.174.199-.347.223-.644.075c-.297-.15-1.255-.463-2.39-1.475c-.883-.788-1.48-1.761-1.653-2.059c-.173-.297-.019-.458.13-.606c.134-.133.297-.347.446-.52s.198-.298.297-.497c.1-.198.05-.371-.025-.52c-.074-.149-.668-1.612-.916-2.207c-.241-.579-.486-.5-.668-.51c-.174-.008-.372-.01-.57-.01s-.52.074-.792.372c-.273.297-1.04 1.016-1.04 2.479c0 1.462 1.064 2.875 1.213 3.074s2.095 3.2 5.076 4.487c.71.306 1.263.489 1.694.625c.712.227 1.36.195 1.872.118c.57-.085 1.758-.719 2.006-1.413s.247-1.289.173-1.413s-.272-.198-.57-.347m-5.422 7.403h-.004a9.87 9.87 0 0 1-5.032-1.378l-.36-.214l-3.742.982l.999-3.648l-.235-.374a9.86 9.86 0 0 1-1.511-5.26c.002-5.45 4.436-9.884 9.889-9.884a9.8 9.8 0 0 1 6.988 2.899a9.82 9.82 0 0 1 2.892 6.992c-.002 5.45-4.436 9.885-9.884 9.885m8.412-18.297A11.82 11.82 0 0 0 11.992 0C5.438 0 .102 5.335.1 11.892a11.86 11.86 0 0 0 1.587 5.945L0 24l6.304-1.654a11.9 11.9 0 0 0 5.684 1.448h.005c6.554 0 11.89-5.335 11.892-11.893a11.82 11.82 0 0 0-3.48-8.413"
|
|
||||||
// clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="SVGXv8lpc2Y">
|
|
||||||
<path fill="#fff" d="M0 0h24v24H0z" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="#"
|
||||||
aria-label="Telegram"
|
aria-label="Share"
|
||||||
className="bg-blue-400 p-4 flex justify-center items-center text-white"
|
className="bg-[#cccccc] p-4 flex justify-center items-center text-white"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 256 256"
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<defs>
|
<path d="m21 12l-7-7v4C7 10 4 15 3 20c2.5-3.5 6-5.1 11-5.1V19z" />
|
||||||
<linearGradient
|
|
||||||
id="SVGuySfwdaH"
|
|
||||||
x1="50%"
|
|
||||||
x2="50%"
|
|
||||||
y1="0%"
|
|
||||||
y2="100%"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
// stop-color="#2aabee"
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
// stop-color="#229ed9"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<path
|
|
||||||
fill="url(#SVGuySfwdaH)"
|
|
||||||
d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#fff"
|
|
||||||
d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 mt-2 text-start">
|
|
||||||
{articleDetail?.slug}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex relative">
|
|
||||||
<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="text-gray-700 leading-relaxed text-justify">
|
||||||
{/* <span className="text-black font-bold text-md">
|
<div
|
||||||
Mikulnews.com -
|
dangerouslySetInnerHTML={{
|
||||||
</span> */}
|
__html: articleDetail?.htmlDescription || "",
|
||||||
|
}}
|
||||||
{articleDetail?.description}
|
/>
|
||||||
</p>
|
</div>
|
||||||
|
{/* <Author /> */}
|
||||||
<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:
|
||||||
|
|
@ -362,19 +382,12 @@ 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">
|
|
||||||
<Image
|
|
||||||
src={"/image-kolom.png"}
|
|
||||||
alt="Berita Utama"
|
|
||||||
fill
|
|
||||||
className="object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<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-red-500">*</span>
|
ditandai <span className="text-orange-600">*</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form className="space-y-6 mt-6">
|
<form className="space-y-6 mt-6">
|
||||||
|
|
@ -383,11 +396,11 @@ export default function DetailContent() {
|
||||||
htmlFor="komentar"
|
htmlFor="komentar"
|
||||||
className="block text-sm font-medium mb-1"
|
className="block text-sm font-medium mb-1"
|
||||||
>
|
>
|
||||||
Komentar <span className="text-red-500">*</span>
|
Komentar <span className="text-orange-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<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-red-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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -397,7 +410,7 @@ export default function DetailContent() {
|
||||||
htmlFor="nama"
|
htmlFor="nama"
|
||||||
className="block text-sm font-medium mb-1"
|
className="block text-sm font-medium mb-1"
|
||||||
>
|
>
|
||||||
Nama <span className="text-red-500">*</span>
|
Nama <span className="text-orange-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -413,7 +426,7 @@ export default function DetailContent() {
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-sm font-medium mb-1"
|
className="block text-sm font-medium mb-1"
|
||||||
>
|
>
|
||||||
Email <span className="text-red-500">*</span>
|
Email <span className="text-orange-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -452,7 +465,7 @@ export default function DetailContent() {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-red-500 hover:bg-red-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2"
|
className="bg-orange-600 hover:bg-orange-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2"
|
||||||
>
|
>
|
||||||
KIRIM KOMENTAR
|
KIRIM KOMENTAR
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -462,30 +475,96 @@ 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="space-y-6">
|
<div className="bg-white shadow p-4 rounded-lg">
|
||||||
{articles?.map((article) => (
|
<Image
|
||||||
<div key={article.id}>
|
src={"/kolom.png"}
|
||||||
<div>
|
alt="Iklan"
|
||||||
<Link
|
width={345}
|
||||||
className="flex space-x-3 mb-2"
|
height={345}
|
||||||
href={`/detail/${article.id}`}
|
className="rounded"
|
||||||
>
|
/>
|
||||||
<Image
|
<button className="mt-4 w-full bg-black text-white py-2 rounded hover:opacity-90">
|
||||||
src={article.thumbnailUrl || "/default-thumb.png"}
|
Learn More
|
||||||
alt={article.title}
|
</button>
|
||||||
width={120}
|
</div>
|
||||||
height={80}
|
|
||||||
className="rounded object-cover w-[120px] h-[80px]"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-bold leading-snug hover:text-red-700">
|
|
||||||
{article.title}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center text-xs text-gray-500 mt-1 space-x-2">
|
<div className="bg-white shadow p-4 rounded-lg">
|
||||||
<span>
|
<h2 className="text-lg font-semibold mb-2">Connect with us</h2>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="bg-[#0057ff] text-white px-3 py-2 rounded">
|
||||||
|
<p className="text-sm font-bold">Bē</p>
|
||||||
|
<p className="text-xs">139 Followers</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#ff0000] text-white px-3 py-2 rounded">
|
||||||
|
<p className="text-sm font-bold">YouTube</p>
|
||||||
|
<p className="text-xs">205k Subscribers</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#f9a825] text-white px-3 py-2 rounded">
|
||||||
|
<p className="text-sm font-bold">RSS</p>
|
||||||
|
<p className="text-xs">23.9k Followers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-base font-semibold mb-2 text-gray-800 border-b pb-1 border-green-600 inline-block">
|
||||||
|
Recommended
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Artikel utama (featured) */}
|
||||||
|
{articles.length > 0 && (
|
||||||
|
<div className="relative">
|
||||||
|
<Link href={`/detail/${articles[0]?.id}`}>
|
||||||
|
<Image
|
||||||
|
src={articles[0]?.thumbnailUrl || "/default.jpg"}
|
||||||
|
alt={articles[0]?.title || "Recommended Article"}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
className="rounded-lg w-full h-auto object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white p-3 rounded-b-lg">
|
||||||
|
<p className="text-sm font-semibold leading-tight">
|
||||||
|
{articles[0]?.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-300 mt-1">
|
||||||
|
📅{" "}
|
||||||
|
{new Date(articles[0]?.createdAt).toLocaleDateString(
|
||||||
|
"id-ID",
|
||||||
|
{
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* List artikel lain */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{articles.slice(1, 4).map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<Link
|
||||||
|
className="flex space-x-3"
|
||||||
|
href={`/detail/${item?.id}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={item.thumbnailUrl || "/default.jpg"}
|
||||||
|
alt={item.title}
|
||||||
|
width={80}
|
||||||
|
height={60}
|
||||||
|
className="rounded object-cover w-[80px] h-[60px]"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold leading-snug">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
📅{" "}
|
📅{" "}
|
||||||
{new Date(article.createdAt).toLocaleDateString(
|
{new Date(item.createdAt).toLocaleDateString(
|
||||||
"id-ID",
|
"id-ID",
|
||||||
{
|
{
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
|
|
@ -493,18 +572,13 @@ export default function DetailContent() {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</span>
|
</p>
|
||||||
<span>💬 0</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
|
||||||
<p className="text-sm text-gray-700 line-clamp-2">
|
|
||||||
{article.description.slice(0, 120)}...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -5,36 +5,166 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function CustomEditor(props) {
|
function CustomEditor(props) {
|
||||||
|
const maxHeight = props.maxHeight || 600;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CKEditor
|
<div className="ckeditor-wrapper">
|
||||||
editor={Editor}
|
<CKEditor
|
||||||
data={props.initialData}
|
editor={Editor}
|
||||||
onChange={(event, editor) => {
|
data={props.initialData}
|
||||||
const data = editor.getData();
|
onChange={(event, editor) => {
|
||||||
console.log({ event, editor, data });
|
const data = editor.getData();
|
||||||
props.onChange(data);
|
console.log({ event, editor, data });
|
||||||
}}
|
props.onChange(data);
|
||||||
config={{
|
}}
|
||||||
toolbar: [
|
config={{
|
||||||
"heading",
|
toolbar: [
|
||||||
"fontsize",
|
"heading",
|
||||||
"bold",
|
"fontsize",
|
||||||
"italic",
|
"bold",
|
||||||
"link",
|
"italic",
|
||||||
"numberedList",
|
"link",
|
||||||
"bulletedList",
|
"numberedList",
|
||||||
"undo",
|
"bulletedList",
|
||||||
"redo",
|
"undo",
|
||||||
"alignment",
|
"redo",
|
||||||
"outdent",
|
"alignment",
|
||||||
"indent",
|
"outdent",
|
||||||
"blockQuote",
|
"indent",
|
||||||
"insertTable",
|
"blockQuote",
|
||||||
"codeBlock",
|
"insertTable",
|
||||||
"sourceEditing",
|
"codeBlock",
|
||||||
],
|
"sourceEditing",
|
||||||
}}
|
],
|
||||||
/>
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #111 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 4px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
height: props.height || 400,
|
||||||
|
removePlugins: ["Title"],
|
||||||
|
mobile: {
|
||||||
|
theme: "silver",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
// Import the optimized editor (choose one based on your migration)
|
||||||
|
// import OptimizedEditor from './optimized-editor'; // TinyMCE
|
||||||
|
// import OptimizedCKEditor from './optimized-ckeditor'; // CKEditor5 Classic
|
||||||
|
// import MinimalEditor from './minimal-editor'; // React Quill
|
||||||
|
|
||||||
|
interface EditorExampleProps {
|
||||||
|
editorType?: 'tinymce' | 'ckeditor' | 'quill';
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditorExample: React.FC<EditorExampleProps> = ({
|
||||||
|
editorType = 'tinymce'
|
||||||
|
}) => {
|
||||||
|
const [content, setContent] = useState('<p>Hello, this is the editor content!</p>');
|
||||||
|
const [savedContent, setSavedContent] = useState('');
|
||||||
|
|
||||||
|
const handleContentChange = (newContent: string) => {
|
||||||
|
setContent(newContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setSavedContent(content);
|
||||||
|
console.log('Content saved:', content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setContent('<p>Content has been reset!</p>');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Rich Text Editor Example</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
This is an optimized editor with {editorType} - much smaller bundle size and better performance!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Editor Panel */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">Editor</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-gray-200 rounded-lg">
|
||||||
|
{/* Choose your editor based on migration */}
|
||||||
|
{editorType === 'tinymce' && (
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-gray-500 text-sm mb-2">
|
||||||
|
TinyMCE Editor (200KB bundle)
|
||||||
|
</p>
|
||||||
|
{/* <OptimizedEditor
|
||||||
|
initialData={content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
height={400}
|
||||||
|
placeholder="Start typing your content..."
|
||||||
|
/> */}
|
||||||
|
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||||
|
<p className="text-gray-500">TinyMCE Editor Component</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editorType === 'ckeditor' && (
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-gray-500 text-sm mb-2">
|
||||||
|
CKEditor5 Classic (800KB bundle)
|
||||||
|
</p>
|
||||||
|
{/* <OptimizedCKEditor
|
||||||
|
initialData={content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
height={400}
|
||||||
|
placeholder="Start typing your content..."
|
||||||
|
/> */}
|
||||||
|
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||||
|
<p className="text-gray-500">CKEditor5 Classic Component</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editorType === 'quill' && (
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-gray-500 text-sm mb-2">
|
||||||
|
React Quill (100KB bundle)
|
||||||
|
</p>
|
||||||
|
{/* <MinimalEditor
|
||||||
|
initialData={content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
height={400}
|
||||||
|
placeholder="Start typing your content..."
|
||||||
|
/> */}
|
||||||
|
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||||
|
<p className="text-gray-500">React Quill Component</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Panel */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Preview</h3>
|
||||||
|
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium mb-2">Current Content:</h4>
|
||||||
|
<div
|
||||||
|
className="prose max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{savedContent && (
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium mb-2">Saved Content:</h4>
|
||||||
|
<div
|
||||||
|
className="prose max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: savedContent }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium mb-2">Raw HTML:</h4>
|
||||||
|
<pre className="text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">
|
||||||
|
{content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Info */}
|
||||||
|
<div className="mt-8 p-4 bg-blue-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-900 mb-2">Performance Benefits:</h4>
|
||||||
|
<ul className="text-sm text-blue-800 space-y-1">
|
||||||
|
<li>• 90% smaller bundle size compared to custom CKEditor5</li>
|
||||||
|
<li>• Faster initial load time</li>
|
||||||
|
<li>• Better mobile performance</li>
|
||||||
|
<li>• Reduced memory usage</li>
|
||||||
|
<li>• Improved Lighthouse score</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditorExample;
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import CustomEditor from './custom-editor';
|
||||||
|
import FormEditor from './form-editor';
|
||||||
|
|
||||||
|
export default function EditorTest() {
|
||||||
|
const [testData, setTestData] = useState('Initial test content');
|
||||||
|
const [editorType, setEditorType] = useState('custom');
|
||||||
|
|
||||||
|
const { control, setValue, watch, handleSubmit } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
title: 'Test Title',
|
||||||
|
description: testData,
|
||||||
|
creatorName: 'Test Creator'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedValues = watch();
|
||||||
|
|
||||||
|
const handleSetValue = () => {
|
||||||
|
const newContent = `<p>Updated content at ${new Date().toLocaleTimeString()}</p><p>This content was set via setValue</p>`;
|
||||||
|
setValue('description', newContent);
|
||||||
|
setTestData(newContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetEmpty = () => {
|
||||||
|
setValue('description', '');
|
||||||
|
setTestData('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetHTML = () => {
|
||||||
|
const htmlContent = `
|
||||||
|
<h2>HTML Content Test</h2>
|
||||||
|
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
|
||||||
|
<ul>
|
||||||
|
<li>List item 1</li>
|
||||||
|
<li>List item 2</li>
|
||||||
|
<li>List item 3</li>
|
||||||
|
</ul>
|
||||||
|
<p>Updated at: ${new Date().toLocaleTimeString()}</p>
|
||||||
|
`;
|
||||||
|
setValue('description', htmlContent);
|
||||||
|
setTestData(htmlContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
console.log('Form submitted:', data);
|
||||||
|
alert('Form submitted! Check console for data.');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
||||||
|
<h1 className="text-2xl font-bold">Editor Test Component</h1>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Editor Type:</Label>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<Button
|
||||||
|
variant={editorType === 'custom' ? 'default' : 'outline'}
|
||||||
|
onClick={() => setEditorType('custom')}
|
||||||
|
>
|
||||||
|
CustomEditor
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={editorType === 'form' ? 'default' : 'outline'}
|
||||||
|
onClick={() => setEditorType('form')}
|
||||||
|
>
|
||||||
|
FormEditor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button onClick={handleSetValue} variant="outline">
|
||||||
|
Set Value (Current Time)
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSetEmpty} variant="outline">
|
||||||
|
Set Empty
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSetHTML} variant="outline">
|
||||||
|
Set HTML Content
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Current Test Data:</Label>
|
||||||
|
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
||||||
|
{testData || '(empty)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Watched Form Values:</Label>
|
||||||
|
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
||||||
|
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Title:</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input {...field} className="mt-1" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Description (Editor):</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
editorType === 'custom' ? (
|
||||||
|
<CustomEditor
|
||||||
|
onChange={field.onChange}
|
||||||
|
initialData={field.value}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormEditor
|
||||||
|
onChange={field.onChange}
|
||||||
|
initialData={field.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Creator Name:</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="creatorName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input {...field} className="mt-1" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Submit Form
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">Instructions:</h3>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||||
|
<li>Switch between CustomEditor and FormEditor to test both</li>
|
||||||
|
<li>Click "Set Value" to test setValue functionality</li>
|
||||||
|
<li>Click "Set Empty" to test empty content handling</li>
|
||||||
|
<li>Click "Set HTML Content" to test rich HTML content</li>
|
||||||
|
<li>Type in the editor to test onChange functionality</li>
|
||||||
|
<li>Submit the form to see all data</li>
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function FormEditor({ onChange, initialData }) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||||
|
const [editorContent, setEditorContent] = useState(initialData || "");
|
||||||
|
|
||||||
|
// Handle editor initialization
|
||||||
|
const handleInit = useCallback((evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
setIsEditorReady(true);
|
||||||
|
|
||||||
|
// Set initial content when editor is ready
|
||||||
|
if (editorContent) {
|
||||||
|
editor.setContent(editorContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle content changes
|
||||||
|
editor.on('change', () => {
|
||||||
|
const content = editor.getContent();
|
||||||
|
setEditorContent(content);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editorContent, onChange]);
|
||||||
|
|
||||||
|
// Watch for initialData changes (from setValue)
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData !== editorContent) {
|
||||||
|
setEditorContent(initialData || "");
|
||||||
|
|
||||||
|
// Update editor content if ready
|
||||||
|
if (editorRef.current && isEditorReady) {
|
||||||
|
editorRef.current.setContent(initialData || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [initialData, editorContent, isEditorReady]);
|
||||||
|
|
||||||
|
// Handle initial data when editor becomes ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditorReady && editorContent && editorRef.current) {
|
||||||
|
editorRef.current.setContent(editorContent);
|
||||||
|
}
|
||||||
|
}, [isEditorReady, editorContent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: 400,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | table | code | help',
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 368px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder: 'Start typing...',
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
||||||
|
paste_retain_style_properties: 'color background-color font-size font-weight',
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: 'bold italic | bullist numlist | link image'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormEditor;
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
// components/minimal-editor.js
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function MinimalEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Set initial content if provided
|
||||||
|
if (props.initialData) {
|
||||||
|
editor.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple onChange handler - no debouncing, no complex logic
|
||||||
|
editor.on('change', () => {
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(editor.getContent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: 400,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | table | code | help',
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 368px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder: 'Start typing...',
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Minimal settings to prevent cursor jumping
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
// Disable problematic features
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
// Basic content handling
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
// Mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: 'bold italic | bullist numlist | link image'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MinimalEditor;
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
interface OptimizedEditorProps {
|
||||||
|
initialData?: string;
|
||||||
|
onChange?: (data: string) => void;
|
||||||
|
height?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
||||||
|
initialData = "",
|
||||||
|
onChange,
|
||||||
|
height = 400,
|
||||||
|
placeholder = "Start typing...",
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
}) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const handleEditorChange = (content: string) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInit = (evt: any, editor: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
initialValue={initialData}
|
||||||
|
onEditorChange={handleEditorChange}
|
||||||
|
disabled={disabled}
|
||||||
|
init={{
|
||||||
|
height,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
"advlist",
|
||||||
|
"autolink",
|
||||||
|
"lists",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"code",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
|
],
|
||||||
|
toolbar:
|
||||||
|
"undo redo | blocks | " +
|
||||||
|
"bold italic forecolor | alignleft aligncenter " +
|
||||||
|
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||||
|
"removeformat | table | code | help",
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: ${height - 32}px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder,
|
||||||
|
// readonly: readOnly,
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Performance optimizations
|
||||||
|
cache_suffix: "?v=1.0",
|
||||||
|
browser_spellcheck: false,
|
||||||
|
gecko_spellcheck: false,
|
||||||
|
// Auto-save feature
|
||||||
|
auto_save: true,
|
||||||
|
auto_save_interval: "30s",
|
||||||
|
// Better mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: "silver",
|
||||||
|
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||||
|
toolbar: "bold italic | bullist numlist | link image",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OptimizedEditor;
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// components/readonly-editor.js
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function ReadOnlyEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Set initial content if provided
|
||||||
|
if (props.initialData) {
|
||||||
|
editor.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable all editing capabilities
|
||||||
|
editor.on('keydown keyup keypress input', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('paste', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable mouse events that might allow editing
|
||||||
|
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
||||||
|
if (e.target.closest('.mce-content-body')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update content when props change
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current && props.initialData) {
|
||||||
|
editorRef.current.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
}, [props.initialData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
initialValue={props.initialData || ''}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: props.height || 400,
|
||||||
|
menubar: false,
|
||||||
|
toolbar: false, // No toolbar for read-only mode
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||||
|
'insertdatetime', 'media', 'table'
|
||||||
|
],
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: ${(props.height || 400) - 32}px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.mce-content-body * {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
readonly: true,
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Minimal settings to prevent cursor jumping
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
// Disable problematic features
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
// Performance optimizations for read-only
|
||||||
|
cache_suffix: '?v=1.0',
|
||||||
|
browser_spellcheck: false,
|
||||||
|
gecko_spellcheck: false,
|
||||||
|
// Disable editing features
|
||||||
|
paste_as_text: true,
|
||||||
|
paste_enable_default_filters: false,
|
||||||
|
paste_word_valid_elements: false,
|
||||||
|
paste_retain_style_properties: false,
|
||||||
|
// Additional read-only settings
|
||||||
|
contextmenu: false,
|
||||||
|
selection: false,
|
||||||
|
// Disable all editing
|
||||||
|
object_resizing: false,
|
||||||
|
element_format: 'html',
|
||||||
|
// Mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadOnlyEditor;
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// components/simple-editor.js
|
||||||
|
|
||||||
|
import React, { useRef, useState, useCallback } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function SimpleEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [editorInstance, setEditorInstance] = useState(null);
|
||||||
|
|
||||||
|
const handleInit = useCallback((evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
setEditorInstance(editor);
|
||||||
|
|
||||||
|
// Set initial content
|
||||||
|
if (props.initialData) {
|
||||||
|
editor.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable automatic content updates
|
||||||
|
editor.settings.auto_focus = false;
|
||||||
|
editor.settings.forced_root_block = 'p';
|
||||||
|
|
||||||
|
// Store the onChange callback
|
||||||
|
editor.onChangeCallback = props.onChange;
|
||||||
|
|
||||||
|
// Handle content changes without triggering re-renders
|
||||||
|
editor.on('change keyup input', (e) => {
|
||||||
|
if (editor.onChangeCallback) {
|
||||||
|
const content = editor.getContent();
|
||||||
|
editor.onChangeCallback(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}, [props.initialData, props.onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: 400,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | table | code | help',
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 368px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder: 'Start typing...',
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Critical settings to prevent cursor jumping
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
keep_styles: true,
|
||||||
|
// Disable problematic features
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
// Better content handling
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
||||||
|
paste_retain_style_properties: 'color background-color font-size font-weight',
|
||||||
|
// Mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: 'bold italic | bullist numlist | link image'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SimpleEditor;
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// components/simple-readonly-editor.js
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function SimpleReadOnlyEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Disable all editing capabilities
|
||||||
|
editor.on('keydown keyup keypress input', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('paste', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable mouse events that might allow editing
|
||||||
|
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
||||||
|
if (e.target.closest('.mce-content-body')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
initialValue={props.initialData || ''}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: props.height || 400,
|
||||||
|
menubar: false,
|
||||||
|
toolbar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||||
|
'insertdatetime', 'media', 'table'
|
||||||
|
],
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: ${(props.height || 400) - 32}px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.mce-content-body * {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
readonly: true,
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
browser_spellcheck: false,
|
||||||
|
gecko_spellcheck: false,
|
||||||
|
paste_as_text: true,
|
||||||
|
paste_enable_default_filters: false,
|
||||||
|
contextmenu: false,
|
||||||
|
selection: false,
|
||||||
|
object_resizing: false,
|
||||||
|
element_format: 'html'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SimpleReadOnlyEditor;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function StableEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const onChangeRef = useRef(props.onChange);
|
||||||
|
|
||||||
|
// Update onChange ref when props change
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeRef.current = props.onChange;
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Set initial content if provided
|
||||||
|
if (props.initialData) {
|
||||||
|
editor.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a simple change handler that doesn't trigger re-renders
|
||||||
|
editor.on('change', () => {
|
||||||
|
if (onChangeRef.current) {
|
||||||
|
onChangeRef.current(editor.getContent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: 400,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | table | code | help',
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 368px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder: 'Start typing...',
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Critical settings for stability
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
keep_styles: true,
|
||||||
|
// Disable all problematic features
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
// Content handling
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
||||||
|
paste_retain_style_properties: 'color background-color font-size font-weight',
|
||||||
|
// Prevent automatic updates
|
||||||
|
element_format: 'html',
|
||||||
|
valid_children: '+body[style]',
|
||||||
|
extended_valid_elements: 'span[*]',
|
||||||
|
custom_elements: '~span',
|
||||||
|
// Mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: 'bold italic | bullist numlist | link image'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StableEditor;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function StaticEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const onChangeRef = useRef(props.onChange);
|
||||||
|
|
||||||
|
// Update onChange ref when props change
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeRef.current = props.onChange;
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Set initial content if provided
|
||||||
|
if (props.initialData) {
|
||||||
|
editor.setContent(props.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a simple change handler that doesn't trigger re-renders
|
||||||
|
editor.on('change', () => {
|
||||||
|
if (onChangeRef.current) {
|
||||||
|
onChangeRef.current(editor.getContent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: 400,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | table | code | help',
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 368px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
placeholder: 'Start typing...',
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
// Critical settings to prevent cursor jumping
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
keep_styles: true,
|
||||||
|
// Disable all problematic features
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
// Content handling
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
||||||
|
paste_retain_style_properties: 'color background-color font-size font-weight',
|
||||||
|
// Prevent automatic updates
|
||||||
|
element_format: 'html',
|
||||||
|
valid_children: '+body[style]',
|
||||||
|
extended_valid_elements: 'span[*]',
|
||||||
|
custom_elements: '~span',
|
||||||
|
// Mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: 'silver',
|
||||||
|
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||||
|
toolbar: 'bold italic | bullist numlist | link image'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StaticEditor;
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
// components/strict-readonly-editor.js
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
function StrictReadOnlyEditor(props) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleInit = (evt, editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Disable all possible editing events
|
||||||
|
const disableEvents = ['keydown', 'keyup', 'keypress', 'input', 'paste', 'drop', 'cut', 'copy'];
|
||||||
|
|
||||||
|
disableEvents.forEach(eventType => {
|
||||||
|
editor.on(eventType, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable mouse events that might allow editing
|
||||||
|
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
|
||||||
|
if (e.target.closest('.mce-content-body')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable focus events
|
||||||
|
editor.on('focus blur', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
onInit={handleInit}
|
||||||
|
initialValue={props.initialData || ''}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={{
|
||||||
|
height: props.height || 400,
|
||||||
|
menubar: false,
|
||||||
|
toolbar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||||
|
'insertdatetime', 'media', 'table'
|
||||||
|
],
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
pointer-events: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: ${(props.height || 400) - 32}px;
|
||||||
|
pointer-events: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
}
|
||||||
|
.mce-content-body * {
|
||||||
|
pointer-events: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
readonly: true,
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: false,
|
||||||
|
auto_focus: false,
|
||||||
|
forced_root_block: 'p',
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
verify_html: false,
|
||||||
|
cleanup: false,
|
||||||
|
cleanup_on_startup: false,
|
||||||
|
auto_resize: false,
|
||||||
|
browser_spellcheck: false,
|
||||||
|
gecko_spellcheck: false,
|
||||||
|
paste_as_text: true,
|
||||||
|
paste_enable_default_filters: false,
|
||||||
|
contextmenu: false,
|
||||||
|
selection: false,
|
||||||
|
object_resizing: false,
|
||||||
|
element_format: 'html',
|
||||||
|
// Additional strict settings
|
||||||
|
valid_children: false,
|
||||||
|
extended_valid_elements: false,
|
||||||
|
custom_elements: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StrictReadOnlyEditor;
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
|
import { Editor } from "@tinymce/tinymce-react";
|
||||||
|
|
||||||
|
interface TinyMCEEditorProps {
|
||||||
|
initialData?: string;
|
||||||
|
onChange?: (data: string) => void;
|
||||||
|
onReady?: (editor: any) => void;
|
||||||
|
height?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
features?: "basic" | "standard" | "full";
|
||||||
|
toolbar?: string;
|
||||||
|
language?: string;
|
||||||
|
uploadUrl?: string;
|
||||||
|
uploadHeaders?: Record<string, string>;
|
||||||
|
className?: string;
|
||||||
|
autoSave?: boolean;
|
||||||
|
autoSaveInterval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||||
|
initialData = "",
|
||||||
|
onChange,
|
||||||
|
onReady,
|
||||||
|
height = 400,
|
||||||
|
placeholder = "Start typing...",
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
features = "standard",
|
||||||
|
toolbar,
|
||||||
|
language = "en",
|
||||||
|
uploadUrl,
|
||||||
|
uploadHeaders,
|
||||||
|
className = "",
|
||||||
|
autoSave = true,
|
||||||
|
autoSaveInterval = 30000,
|
||||||
|
}) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
||||||
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||||
|
const [wordCount, setWordCount] = useState(0);
|
||||||
|
|
||||||
|
// Feature-based configurations
|
||||||
|
const getFeatureConfig = (featureLevel: string) => {
|
||||||
|
const configs = {
|
||||||
|
basic: {
|
||||||
|
plugins: ["lists", "link", "autolink", "wordcount"],
|
||||||
|
toolbar: "bold italic | bullist numlist | link",
|
||||||
|
menubar: false,
|
||||||
|
},
|
||||||
|
standard: {
|
||||||
|
plugins: [
|
||||||
|
"advlist",
|
||||||
|
"autolink",
|
||||||
|
"lists",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
|
],
|
||||||
|
toolbar:
|
||||||
|
"undo redo | blocks | " +
|
||||||
|
"bold italic forecolor | alignleft aligncenter " +
|
||||||
|
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||||
|
"removeformat | table | code | help",
|
||||||
|
menubar: false,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
plugins: [
|
||||||
|
"advlist",
|
||||||
|
"autolink",
|
||||||
|
"lists",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"charmap",
|
||||||
|
"preview",
|
||||||
|
"anchor",
|
||||||
|
"searchreplace",
|
||||||
|
"visualblocks",
|
||||||
|
"code",
|
||||||
|
"fullscreen",
|
||||||
|
"insertdatetime",
|
||||||
|
"media",
|
||||||
|
"table",
|
||||||
|
"help",
|
||||||
|
"wordcount",
|
||||||
|
"emoticons",
|
||||||
|
"paste",
|
||||||
|
"textcolor",
|
||||||
|
"colorpicker",
|
||||||
|
"hr",
|
||||||
|
"pagebreak",
|
||||||
|
"nonbreaking",
|
||||||
|
"toc",
|
||||||
|
"imagetools",
|
||||||
|
"textpattern",
|
||||||
|
"codesample",
|
||||||
|
],
|
||||||
|
toolbar:
|
||||||
|
"undo redo | formatselect | bold italic backcolor | " +
|
||||||
|
"alignleft aligncenter alignright alignjustify | " +
|
||||||
|
"bullist numlist outdent indent | removeformat | help",
|
||||||
|
menubar: "file edit view insert format tools table help",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return configs[featureLevel as keyof typeof configs] || configs.standard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorChange = (content: string) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorInit = (evt: any, editor: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
setIsEditorLoaded(true);
|
||||||
|
|
||||||
|
if (onReady) {
|
||||||
|
onReady(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up word count tracking
|
||||||
|
editor.on("keyup", () => {
|
||||||
|
const count = editor.plugins.wordcount.body.getCharacterCount();
|
||||||
|
setWordCount(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up auto-save
|
||||||
|
if (autoSave && !readOnly) {
|
||||||
|
setInterval(() => {
|
||||||
|
const content = editor.getContent();
|
||||||
|
localStorage.setItem("tinymce-autosave", content);
|
||||||
|
setLastSaved(new Date());
|
||||||
|
}, autoSaveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix cursor jumping issues
|
||||||
|
editor.on("keyup", (e: any) => {
|
||||||
|
// Prevent cursor jumping on content changes
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on("input", (e: any) => {
|
||||||
|
// Prevent unnecessary re-renders
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle paste events properly
|
||||||
|
editor.on("paste", (e: any) => {
|
||||||
|
// Allow default paste behavior
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageUpload = (blobInfo: any, progress: any) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!uploadUrl) {
|
||||||
|
reject("No upload URL configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", blobInfo.blob(), blobInfo.filename());
|
||||||
|
|
||||||
|
fetch(uploadUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: uploadHeaders || {},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((result) => {
|
||||||
|
resolve(result.url);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureConfig = getFeatureConfig(features);
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
height,
|
||||||
|
language,
|
||||||
|
placeholder,
|
||||||
|
// readonly: readOnly,
|
||||||
|
// disabled,
|
||||||
|
branding: false,
|
||||||
|
elementpath: false,
|
||||||
|
resize: false,
|
||||||
|
statusbar: !readOnly,
|
||||||
|
// Performance optimizations
|
||||||
|
cache_suffix: "?v=1.0",
|
||||||
|
browser_spellcheck: false,
|
||||||
|
gecko_spellcheck: false,
|
||||||
|
// Content styling
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.mce-content-body {
|
||||||
|
min-height: ${height - 32}px;
|
||||||
|
}
|
||||||
|
.mce-content-body:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
// Image upload configuration
|
||||||
|
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
|
||||||
|
automatic_uploads: !!uploadUrl,
|
||||||
|
file_picker_types: "image",
|
||||||
|
// Better mobile support
|
||||||
|
mobile: {
|
||||||
|
theme: "silver",
|
||||||
|
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||||
|
toolbar: "bold italic | bullist numlist | link image",
|
||||||
|
},
|
||||||
|
// Paste configuration
|
||||||
|
paste_as_text: false,
|
||||||
|
paste_enable_default_filters: true,
|
||||||
|
paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6",
|
||||||
|
paste_retain_style_properties:
|
||||||
|
"color background-color font-size font-weight",
|
||||||
|
// Table configuration
|
||||||
|
table_default_styles: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
table_default_attributes: {
|
||||||
|
border: "1",
|
||||||
|
},
|
||||||
|
// Code configuration
|
||||||
|
codesample_languages: [
|
||||||
|
{ text: "HTML/XML", value: "markup" },
|
||||||
|
{ text: "JavaScript", value: "javascript" },
|
||||||
|
{ text: "CSS", value: "css" },
|
||||||
|
{ text: "PHP", value: "php" },
|
||||||
|
{ text: "Python", value: "python" },
|
||||||
|
{ text: "Java", value: "java" },
|
||||||
|
{ text: "C", value: "c" },
|
||||||
|
{ text: "C++", value: "cpp" },
|
||||||
|
],
|
||||||
|
// ...feature config
|
||||||
|
...featureConfig,
|
||||||
|
// Custom toolbar if provided
|
||||||
|
...(toolbar && { toolbar }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`tinymce-editor-container ${className}`}>
|
||||||
|
<Editor
|
||||||
|
onInit={handleEditorInit}
|
||||||
|
initialValue={initialData}
|
||||||
|
onEditorChange={handleEditorChange}
|
||||||
|
disabled={disabled || readOnly}
|
||||||
|
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||||
|
init={editorConfig}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Status bar */}
|
||||||
|
{isEditorLoaded && (
|
||||||
|
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span>
|
||||||
|
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
|
||||||
|
</span>
|
||||||
|
{lastSaved && autoSave && !readOnly && (
|
||||||
|
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
||||||
|
)}
|
||||||
|
<span>• {wordCount} characters</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
{features} mode
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Performance indicator */}
|
||||||
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
|
Bundle size:{" "}
|
||||||
|
{features === "basic"
|
||||||
|
? "~150KB"
|
||||||
|
: features === "standard"
|
||||||
|
? "~200KB"
|
||||||
|
: "~300KB"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TinyMCEEditor;
|
||||||
|
|
@ -3,17 +3,261 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||||
|
|
||||||
function ViewEditor(props) {
|
function ViewEditor(props) {
|
||||||
|
const maxHeight = props.maxHeight || 600; // Default max height 600px
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CKEditor
|
<div className="ckeditor-view-wrapper">
|
||||||
editor={Editor}
|
<CKEditor
|
||||||
data={props.initialData}
|
editor={Editor}
|
||||||
disabled={true}
|
data={props.initialData}
|
||||||
config={{
|
disabled={true}
|
||||||
// toolbar: [],
|
config={{
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
}}
|
content_style: `
|
||||||
/>
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #111;
|
||||||
|
background: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 4px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
height: props.height || 400,
|
||||||
|
removePlugins: ["Title"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style jsx>{`
|
||||||
|
.ckeditor-view-wrapper {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||||
|
min-height: ${props.height || 400}px;
|
||||||
|
max-height: ${maxHeight}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
min-height: ${(props.height || 400) - 50}px;
|
||||||
|
max-height: ${maxHeight - 50}px;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
background-color: #fdfdfd;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🌙 Dark mode support */
|
||||||
|
:global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
background-color: #111 !important;
|
||||||
|
color: #f9fafb !important;
|
||||||
|
border-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h1,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h2,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h3,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h4,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h5,
|
||||||
|
:global(.dark) .ckeditor-view-wrapper h6 {
|
||||||
|
color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .ckeditor-view-wrapper blockquote {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
border-left: 4px solid #374151 !important;
|
||||||
|
color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling */
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🌙 Dark mode scrollbar */
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark)
|
||||||
|
.ckeditor-view-wrapper
|
||||||
|
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read-only specific styling */
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide toolbar */
|
||||||
|
.ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewEditor;
|
export default ViewEditor;
|
||||||
|
|
||||||
|
// import React from "react";
|
||||||
|
// import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||||
|
// import Editor from "ckeditor5-custom-build";
|
||||||
|
|
||||||
|
// function ViewEditor(props) {
|
||||||
|
// const maxHeight = props.maxHeight || 600;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="ckeditor-view-wrapper">
|
||||||
|
// <CKEditor
|
||||||
|
// editor={Editor}
|
||||||
|
// data={props.initialData}
|
||||||
|
// disabled={true}
|
||||||
|
// config={{
|
||||||
|
// // toolbar: [],
|
||||||
|
// isReadOnly: true,
|
||||||
|
// // Add content styling configuration for read-only mode
|
||||||
|
// content_style: `
|
||||||
|
// body {
|
||||||
|
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
// font-size: 14px;
|
||||||
|
// line-height: 1.6;
|
||||||
|
// color: #333;
|
||||||
|
// margin: 0;
|
||||||
|
// padding: 0;
|
||||||
|
// }
|
||||||
|
// p {
|
||||||
|
// margin: 0.5em 0;
|
||||||
|
// }
|
||||||
|
// h1, h2, h3, h4, h5, h6 {
|
||||||
|
// margin: 1em 0 0.5em 0;
|
||||||
|
// }
|
||||||
|
// ul, ol {
|
||||||
|
// margin: 0.5em 0;
|
||||||
|
// padding-left: 2em;
|
||||||
|
// }
|
||||||
|
// blockquote {
|
||||||
|
// margin: 1em 0;
|
||||||
|
// padding: 0.5em 1em;
|
||||||
|
// border-left: 4px solid #d1d5db;
|
||||||
|
// background-color: #f9fafb;
|
||||||
|
// }
|
||||||
|
// `,
|
||||||
|
// // Editor appearance settings
|
||||||
|
// height: props.height || 400,
|
||||||
|
// removePlugins: ['Title'],
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// <style jsx>{`
|
||||||
|
// .ckeditor-view-wrapper {
|
||||||
|
// border-radius: 6px;
|
||||||
|
// overflow: hidden;
|
||||||
|
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||||
|
// min-height: ${props.height || 400}px;
|
||||||
|
// max-height: ${maxHeight}px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||||
|
// min-height: ${(props.height || 400) - 50}px;
|
||||||
|
// max-height: ${maxHeight - 50}px;
|
||||||
|
// overflow-y: auto !important;
|
||||||
|
// scrollbar-width: thin;
|
||||||
|
// scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
// background-color:rgb(253, 253, 253);
|
||||||
|
// border: 1px solid #d1d5db;
|
||||||
|
// border-radius: 6px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Custom scrollbar styling for webkit browsers */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||||
|
// width: 8px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||||
|
// background: #f1f5f9;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||||
|
// background: #cbd5e1;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||||
|
// background: #94a3b8;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Ensure content doesn't overflow */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
||||||
|
// overflow: hidden;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Read-only specific styling */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||||
|
// background-color: #f8fafc;
|
||||||
|
// color: #4b5563;
|
||||||
|
// cursor: default;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Hide toolbar for view-only mode */
|
||||||
|
// .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||||
|
// display: none !important;
|
||||||
|
// }
|
||||||
|
// `}</style>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default ViewEditor;
|
||||||
|
|
|
||||||
|
|
@ -524,13 +524,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 : ""
|
||||||
|
|
@ -664,7 +672,10 @@ export default function CreateArticleForm() {
|
||||||
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
|
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
|
||||||
}}
|
}}
|
||||||
classNamePrefix="select"
|
classNamePrefix="select"
|
||||||
onChange={onChange}
|
value={value}
|
||||||
|
onChange={(selected) => {
|
||||||
|
onChange(selected);
|
||||||
|
}}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
components={animatedComponents}
|
components={animatedComponents}
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
|
|
@ -683,60 +694,7 @@ export default function CreateArticleForm() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm">Tags</p>
|
<p className="text-sm">Tags</p>
|
||||||
{/* <Controller
|
|
||||||
control={control}
|
|
||||||
name="tags"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Textarea
|
|
||||||
type="text"
|
|
||||||
id="tags"
|
|
||||||
placeholder=""
|
|
||||||
label=""
|
|
||||||
value={tag}
|
|
||||||
onValueChange={setTag}
|
|
||||||
startContent={
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{value.map((item, index) => (
|
|
||||||
<Chip
|
|
||||||
color="primary"
|
|
||||||
key={index}
|
|
||||||
className=""
|
|
||||||
onClose={() => {
|
|
||||||
const filteredTags = value.filter((tag) => tag !== item);
|
|
||||||
if (filteredTags.length === 0) {
|
|
||||||
setError("tags", {
|
|
||||||
type: "manual",
|
|
||||||
message: "Tags tidak boleh kosong",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clearErrors("tags");
|
|
||||||
setValue("tags", filteredTags as [string, ...string[]]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
if (tag.trim() !== "") {
|
|
||||||
setValue("tags", [...value, tag.trim()]);
|
|
||||||
setTag("");
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
labelPlacement="outside"
|
|
||||||
className="w-full h-fit"
|
|
||||||
classNames={{
|
|
||||||
inputWrapper: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
|
|
||||||
}}
|
|
||||||
variant="bordered"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/> */}
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="tags"
|
name="tags"
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">Deskripsi</p>
|
<p className="text-sm mt-3">Deskripsi</p>
|
||||||
<Controller
|
{/* <Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field: { onChange, value } }) =>
|
render={({ field: { onChange, value } }) =>
|
||||||
|
|
@ -572,8 +572,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
<p className="text-red-400 text-sm mb-3">
|
<p className="text-red-400 text-sm mb-3">
|
||||||
{errors.description?.message}
|
{errors.description?.message}
|
||||||
</p>
|
</p>
|
||||||
|
)} */}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<CustomEditor onChange={field.onChange} initialData={field.value} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.description?.message && (
|
||||||
|
<p className="text-red-400 text-sm">{errors.description.message}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm mt-3">File Media</p>
|
<p className="text-sm mt-3">File Media</p>
|
||||||
{!isDetail && (
|
{!isDetail && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
@ -672,6 +681,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"
|
||||||
|
|
@ -943,14 +953,11 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isDetail &&
|
{detailData?.isPublish === false && (
|
||||||
detailData?.isPublish === false &&
|
<Button type="button" color="primary" onClick={doPublish}>
|
||||||
detailData?.statusId !== 1 &&
|
Publish
|
||||||
Number(userId) === detailData?.createdById && (
|
</Button>
|
||||||
<Button type="button" color="primary" onClick={doPublish}>
|
)}
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{/* {!isDetail && (
|
{/* {!isDetail && (
|
||||||
<Button color="success" type="button">
|
<Button color="success" type="button">
|
||||||
<p className="text-white">Draft</p>
|
<p className="text-white">Draft</p>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export default function GenerateSingleArticleForm(props: {
|
||||||
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) {
|
||||||
|
|
@ -319,6 +319,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 +351,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 +375,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 +420,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}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
|
|
@ -50,38 +51,42 @@ export default function BannerNews() {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 p-4">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 p-4">
|
||||||
{/* Artikel utama */}
|
{/* Artikel utama */}
|
||||||
<div className="lg:col-span-2 relative">
|
<div className="lg:col-span-2 relative">
|
||||||
<img
|
<Link href={`/detail/${mainArticle?.id}`}>
|
||||||
src={mainArticle.thumbnailUrl || mainArticle.files?.[0]?.fileUrl}
|
<img
|
||||||
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
|
src={mainArticle.thumbnailUrl || mainArticle.files?.[0]?.fileUrl}
|
||||||
className="w-full h-[400px] object-cover rounded-lg"
|
alt={mainArticle.files?.[0]?.file_alt || mainArticle.title}
|
||||||
/>
|
className="w-full h-[400px] object-cover rounded-lg"
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-6 rounded-b-lg">
|
/>
|
||||||
<span className="bg-yellow-500 text-white text-xs px-2 py-1 rounded">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-6 rounded-b-lg">
|
||||||
{mainArticle.categoryName || "Berita Terkini"}
|
<span className="bg-yellow-500 text-white text-xs px-2 py-1 rounded">
|
||||||
</span>
|
{mainArticle.categoryName || "Berita Terkini"}
|
||||||
<h2 className="mt-2 text-white text-2xl font-bold">
|
</span>
|
||||||
{mainArticle.title}
|
<h2 className="mt-2 text-white text-2xl font-bold">
|
||||||
</h2>
|
{mainArticle.title}
|
||||||
</div>
|
</h2>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Artikel samping */}
|
{/* Artikel samping */}
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{sideArticles.map((article) => (
|
{sideArticles.map((article) => (
|
||||||
<div key={article.id} className="relative">
|
<div key={article.id} className="relative">
|
||||||
<img
|
<Link href={`/detail/${article?.id}`}>
|
||||||
src={article.thumbnailUrl || article.files?.[0]?.fileUrl}
|
<img
|
||||||
alt={article.files?.[0]?.file_alt || article.title}
|
src={article.thumbnailUrl || article.files?.[0]?.fileUrl}
|
||||||
className="w-full h-[125px] object-cover rounded-lg"
|
alt={article.files?.[0]?.file_alt || article.title}
|
||||||
/>
|
className="w-full h-[125px] object-cover rounded-lg"
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4 rounded-b-lg">
|
/>
|
||||||
<span className="bg-yellow-500 text-white text-xs px-2 py-1 rounded">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4 rounded-b-lg">
|
||||||
{article.categoryName || "Berita Terkini"}
|
<span className="bg-yellow-500 text-white text-xs px-2 py-1 rounded">
|
||||||
</span>
|
{article.categoryName || "Berita Terkini"}
|
||||||
<h3 className="mt-1 text-white text-sm font-semibold">
|
</span>
|
||||||
{article.title}
|
<h3 className="mt-1 text-white text-sm font-semibold">
|
||||||
</h3>
|
{article.title}
|
||||||
</div>
|
</h3>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Calendar, Eye, MessageSquare } from "lucide-react";
|
import { Calendar, Eye, MessageSquare } from "lucide-react";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
// Definisi type Article
|
// Definisi type Article
|
||||||
type Article = {
|
type Article = {
|
||||||
|
|
@ -78,45 +79,47 @@ export default function BreakingNews() {
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="bg-white border overflow-hidden shadow-sm hover:shadow-md transition"
|
className="bg-white border overflow-hidden shadow-sm hover:shadow-md transition"
|
||||||
>
|
>
|
||||||
<div className="relative w-full h-40">
|
<Link href={`/detail/${article?.id}`}>
|
||||||
<Image
|
<div className="relative w-full h-40">
|
||||||
src={
|
<Image
|
||||||
article.thumbnailUrl ||
|
src={
|
||||||
article.files?.[0]?.fileUrl ||
|
article.thumbnailUrl ||
|
||||||
"/placeholder.jpg"
|
article.files?.[0]?.fileUrl ||
|
||||||
}
|
"/placeholder.jpg"
|
||||||
alt={article.files?.[0]?.file_alt || article.title}
|
}
|
||||||
fill
|
alt={article.files?.[0]?.file_alt || article.title}
|
||||||
className="object-cover"
|
fill
|
||||||
/>
|
className="object-cover"
|
||||||
<span className="absolute top-2 left-2 bg-black text-white text-xs px-2 py-1 rounded">
|
/>
|
||||||
{article.categoryName || article.categories?.[0]?.title}
|
<span className="absolute top-2 left-2 bg-black text-white text-xs px-2 py-1 rounded">
|
||||||
</span>
|
{article.categoryName || article.categories?.[0]?.title}
|
||||||
</div>
|
|
||||||
<div className="p-3">
|
|
||||||
<h3 className="font-semibold text-base leading-snug mb-2 line-clamp-2">
|
|
||||||
{article.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 line-clamp-2 mb-3">
|
|
||||||
{article.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="w-3 h-3" />{" "}
|
|
||||||
{new Date(article.createdAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Eye className="w-3 h-3" /> {0}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<MessageSquare className="w-3 h-3" /> {0}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="p-3">
|
||||||
|
<h3 className="font-semibold text-base leading-snug mb-2 line-clamp-2">
|
||||||
|
{article.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2 mb-3">
|
||||||
|
{article.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />{" "}
|
||||||
|
{new Date(article.createdAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Eye className="w-3 h-3" /> {0}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MessageSquare className="w-3 h-3" /> {0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Image from "next/image";
|
||||||
import { Calendar, Eye, MessageSquare } from "lucide-react";
|
import { Calendar, Eye, MessageSquare } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -57,39 +58,41 @@ export default function Header() {
|
||||||
key={post.id}
|
key={post.id}
|
||||||
className="relative group overflow-hidden h-56 md:h-64"
|
className="relative group overflow-hidden h-56 md:h-64"
|
||||||
>
|
>
|
||||||
<Image
|
<Link href={`/detail/${post?.id}`}>
|
||||||
src={post.thumbnailUrl || "/default.jpg"}
|
<Image
|
||||||
alt={post.title}
|
src={post.thumbnailUrl || "/default.jpg"}
|
||||||
fill
|
alt={post.title}
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
fill
|
||||||
/>
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
<div className="absolute inset-0 bg-black/40"></div>
|
/>
|
||||||
<div className="absolute bottom-3 left-3 right-3 text-white">
|
<div className="absolute inset-0 bg-black/40"></div>
|
||||||
<span className="bg-black text-white text-xs px-2 py-1 rounded">
|
<div className="absolute bottom-3 left-3 right-3 text-white">
|
||||||
{post.categoryName || post.categories?.[0]?.title}
|
<span className="bg-black text-white text-xs px-2 py-1 rounded">
|
||||||
</span>
|
{post.categoryName || post.categories?.[0]?.title}
|
||||||
<h2 className="mt-2 font-semibold text-lg leading-snug">
|
|
||||||
{post.title}
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center gap-4 text-xs mt-2 opacity-90">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="w-3 h-3" />{" "}
|
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Eye className="w-3 h-3" /> {/* belum ada views di API */}
|
|
||||||
0
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<MessageSquare className="w-3 h-3" />{" "}
|
|
||||||
{/* belum ada comments */}0
|
|
||||||
</span>
|
</span>
|
||||||
|
<h2 className="mt-2 font-semibold text-lg leading-snug">
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-4 text-xs mt-2 opacity-90">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />{" "}
|
||||||
|
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Eye className="w-3 h-3" />{" "}
|
||||||
|
{/* belum ada views di API */}0
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MessageSquare className="w-3 h-3" />{" "}
|
||||||
|
{/* belum ada comments */}0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,37 +104,39 @@ export default function Header() {
|
||||||
key={post.id}
|
key={post.id}
|
||||||
className="relative group overflow-hidden h-56 md:h-64"
|
className="relative group overflow-hidden h-56 md:h-64"
|
||||||
>
|
>
|
||||||
<Image
|
<Link href={`/detail/${post?.id}`}>
|
||||||
src={post.thumbnailUrl || "/default.jpg"}
|
<Image
|
||||||
alt={post.title}
|
src={post.thumbnailUrl || "/default.jpg"}
|
||||||
fill
|
alt={post.title}
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
fill
|
||||||
/>
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
<div className="absolute inset-0 bg-black/40"></div>
|
/>
|
||||||
<div className="absolute bottom-3 left-3 right-3 text-white">
|
<div className="absolute inset-0 bg-black/40"></div>
|
||||||
<span className="bg-black text-white text-xs px-2 py-1 rounded">
|
<div className="absolute bottom-3 left-3 right-3 text-white">
|
||||||
{post.categoryName || post.categories?.[0]?.title}
|
<span className="bg-black text-white text-xs px-2 py-1 rounded">
|
||||||
</span>
|
{post.categoryName || post.categories?.[0]?.title}
|
||||||
<h2 className="mt-2 font-semibold text-lg leading-snug">
|
|
||||||
{post.title}
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center gap-4 text-xs mt-2 opacity-90">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="w-3 h-3" />{" "}
|
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Eye className="w-3 h-3" /> 0
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<MessageSquare className="w-3 h-3" /> 0
|
|
||||||
</span>
|
</span>
|
||||||
|
<h2 className="mt-2 font-semibold text-lg leading-snug">
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-4 text-xs mt-2 opacity-90">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />{" "}
|
||||||
|
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Eye className="w-3 h-3" /> 0
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MessageSquare className="w-3 h-3" /> 0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
type Article = {
|
type Article = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -18,6 +20,8 @@ type Article = {
|
||||||
export default function LatestNews() {
|
export default function LatestNews() {
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [showData, setShowData] = useState("5");
|
const [showData, setShowData] = useState("5");
|
||||||
|
const pathname = usePathname();
|
||||||
|
const currentSlug = pathname.split("/")[2] || "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchArticles();
|
fetchArticles();
|
||||||
|
|
@ -47,80 +51,53 @@ export default function LatestNews() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 py-10 grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="max-w-7xl mx-auto px-4 py-10 grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* KIRI: Artikel utama dan kedua */}
|
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="text-3xl font-bold mb-2">Berita Terkini</h1>
|
<h1 className="text-3xl font-bold mb-2">
|
||||||
|
{" "}
|
||||||
|
{currentSlug ? ` ${currentSlug}` : "Berita Terkini"}
|
||||||
|
</h1>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 mb-6">
|
||||||
Kolom ini berisi berita-berita yang saat ini sedang menjadi sorotan
|
Kolom ini berisi berita-berita yang saat ini sedang menjadi sorotan
|
||||||
atau terkait dengan peristiwa terbaru.
|
atau terkait dengan peristiwa terbaru.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mainArticle && (
|
{articles.slice(0, 5).map((article, index) => (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
<Link
|
||||||
|
key={article.id}
|
||||||
|
href={`/detail/${article.id}`}
|
||||||
|
className={`grid grid-cols-1 md:grid-cols-2 gap-4 mb-6 items-center`}
|
||||||
|
>
|
||||||
{/* Gambar */}
|
{/* Gambar */}
|
||||||
<div className="relative h-64 w-full">
|
<div className={`relative ${index === 0 ? "h-64" : "h-64"} w-full`}>
|
||||||
<Image
|
<Image
|
||||||
src={mainArticle.thumbnailUrl}
|
src={article.thumbnailUrl}
|
||||||
alt={mainArticle.title}
|
alt={article.title}
|
||||||
fill
|
fill
|
||||||
className="object-cover rounded"
|
className="object-cover rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Konten */}
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<span className="bg-yellow-500 text-white px-2 py-1 text-xs font-semibold w-fit mb-2">
|
|
||||||
BERITA TERKINI
|
|
||||||
</span>
|
|
||||||
<h2 className="text-xl font-bold text-orange-600 mb-2">
|
|
||||||
{mainArticle.title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs text-gray-500 mb-2">
|
|
||||||
by {mainArticle.createdByName} •{" "}
|
|
||||||
{formatDate(mainArticle.createdAt)} • 💬 0
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-700 mb-3 line-clamp-3">
|
|
||||||
{mainArticle.description}
|
|
||||||
</p>
|
|
||||||
<button className="bg-black text-white text-xs px-4 py-2 w-fit hover:bg-gray-800">
|
|
||||||
READ MORE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Artikel Kedua */}
|
|
||||||
{secondArticle && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Gambar */}
|
|
||||||
<div className="relative h-48 w-full">
|
|
||||||
<Image
|
|
||||||
src={secondArticle.thumbnailUrl}
|
|
||||||
alt={secondArticle.title}
|
|
||||||
fill
|
|
||||||
className="object-cover rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Konten */}
|
{/* Konten */}
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<span className="bg-yellow-500 text-white px-2 py-1 text-xs font-semibold w-fit mb-2">
|
<span className="bg-yellow-500 text-white px-2 py-1 text-xs font-semibold w-fit mb-2">
|
||||||
BERITA TERKINI
|
BERITA TERKINI
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-xl font-bold text-gray-800 mb-2">
|
<h2 className="text-xl font-bold hover:text-red-500 mb-2">
|
||||||
{secondArticle.title}
|
{article.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500 mb-2">
|
<p className="text-xs text-gray-500 mb-2">
|
||||||
by {secondArticle.createdByName} •{" "}
|
by {article.createdByName} • {formatDate(article.createdAt)} •
|
||||||
{formatDate(secondArticle.createdAt)} • 💬 0
|
💬 0
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-3 line-clamp-3">
|
<p className="text-sm text-gray-700 mb-3 line-clamp-3">
|
||||||
{secondArticle.description}
|
{article.description}
|
||||||
</p>
|
</p>
|
||||||
<button className="bg-black text-white text-xs px-4 py-2 w-fit hover:bg-gray-800">
|
<button className="bg-black text-white text-xs px-4 py-2 w-fit hover:bg-gray-800">
|
||||||
READ MORE
|
READ MORE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KANAN: Advertisement */}
|
{/* KANAN: Advertisement */}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getListArticle } from "@/service/article";
|
import { getListArticle } from "@/service/article";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
// Type dari API
|
// Type dari API
|
||||||
type Article = {
|
type Article = {
|
||||||
|
|
@ -130,41 +131,44 @@ export default function News() {
|
||||||
function PostItem({ post }: { post: Article }) {
|
function PostItem({ post }: { post: Article }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 mb-6 pb-4">
|
<div className="flex gap-4 mb-6 pb-4">
|
||||||
{/* Gambar */}
|
<Link className="flex items-center gap-4 " href={`/detail/${post?.id}`}>
|
||||||
<div className="relative w-40 h-28 flex-shrink-0">
|
<div className="relative w-40 h-28 flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
post.thumbnailUrl || post.files?.[0]?.fileUrl || "/placeholder.jpg"
|
post.thumbnailUrl ||
|
||||||
}
|
post.files?.[0]?.fileUrl ||
|
||||||
alt={post.files?.[0]?.file_alt || post.title}
|
"/placeholder.jpg"
|
||||||
fill
|
}
|
||||||
className="object-cover"
|
alt={post.files?.[0]?.file_alt || post.title}
|
||||||
/>
|
fill
|
||||||
</div>
|
className="object-cover"
|
||||||
|
/>
|
||||||
{/* Konten */}
|
|
||||||
<div className="flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-lg mb-1 hover:text-[#3677A8] cursor-pointer">
|
|
||||||
{post.title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs text-gray-500 mb-1">
|
|
||||||
by {post.createdByName} •{" "}
|
|
||||||
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}{" "}
|
|
||||||
• 💬 0
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 mb-2 line-clamp-2">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button className="bg-black text-white text-xs px-3 py-1 hover:bg-gray-800 w-fit">
|
|
||||||
Read more
|
{/* Konten */}
|
||||||
</button>
|
<div className="flex flex-col justify-between">
|
||||||
</div>
|
<div>
|
||||||
|
<h2 className="font-semibold text-lg mb-1 hover:text-[#3677A8] cursor-pointer">
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-gray-500 mb-1">
|
||||||
|
by {post.createdByName} •{" "}
|
||||||
|
{new Date(post.createdAt).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}{" "}
|
||||||
|
• 💬 0
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mb-2 line-clamp-2">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="bg-black text-white text-xs px-3 py-1 hover:bg-gray-800 w-fit">
|
||||||
|
Read more
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,11 +173,11 @@ export default function ArticleTable() {
|
||||||
initState();
|
initState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyUrlArticle = async (id: number, slug: string) => {
|
const copyUrlArticle = async (id: number) => {
|
||||||
const url =
|
const url =
|
||||||
`${window.location.protocol}//${window.location.host}` +
|
`${window.location.protocol}//${window.location.host}` +
|
||||||
"/news/detail/" +
|
"/detail/" +
|
||||||
`${id}-${slug}`;
|
`${id}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
successToast("Success", "Article Copy to Clipboard");
|
successToast("Success", "Article Copy to Clipboard");
|
||||||
|
|
@ -228,9 +228,7 @@ export default function ArticleTable() {
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => copyUrlArticle(article.id)}>
|
||||||
onClick={() => copyUrlArticle(article.id, article.slug)}
|
|
||||||
>
|
|
||||||
<CopyIcon className="mr-2 h-4 w-4" />
|
<CopyIcon className="mr-2 h-4 w-4" />
|
||||||
Copy Url Article
|
Copy Url Article
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,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.3.0",
|
||||||
"@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",
|
||||||
|
|
@ -9049,6 +9050,111 @@
|
||||||
"fast-glob": "3.3.1"
|
"fast-glob": "3.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
|
"version": "15.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
|
||||||
|
"integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "15.5.3",
|
"version": "15.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
|
||||||
|
|
@ -10149,6 +10255,24 @@
|
||||||
"tailwindcss": "4.1.13"
|
"tailwindcss": "4.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tinymce/tinymce-react": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||||
|
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||||
|
"tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"tinymce": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/color-convert": {
|
"node_modules/@types/color-convert": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
|
||||||
|
|
@ -16747,111 +16871,6 @@
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
|
||||||
"version": "15.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
|
|
||||||
"integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
package.json
19
package.json
|
|
@ -23,34 +23,35 @@
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@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.3.0",
|
||||||
"@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",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"framer-motion": "^12.20.1",
|
"framer-motion": "^12.20.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lightningcss": "^1.30.1",
|
"lightningcss": "^1.30.1",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
|
"next": "15.5.3",
|
||||||
|
"react": "19.1.0",
|
||||||
"react-apexcharts": "^1.7.0",
|
"react-apexcharts": "^1.7.0",
|
||||||
"react-datepicker": "^8.4.0",
|
"react-datepicker": "^8.4.0",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.7.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.59.0",
|
||||||
"react-password-checklist": "^1.8.1",
|
"react-password-checklist": "^1.8.1",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.1",
|
||||||
"sweetalert2": "^11.22.2",
|
"sweetalert2": "^11.22.2",
|
||||||
"sweetalert2-react-content": "^5.1.0",
|
"sweetalert2-react-content": "^5.1.0",
|
||||||
"zod": "^3.25.67",
|
"tailwind-merge": "^3.3.1",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"zod": "^3.25.67"
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"lucide-react": "^0.544.0",
|
|
||||||
"next": "15.5.3",
|
|
||||||
"react": "19.1.0",
|
|
||||||
"react-dom": "19.1.0",
|
|
||||||
"tailwind-merge": "^3.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue