This commit is contained in:
Anang Yusman 2026-01-14 11:07:46 +08:00
parent fe4b0bd917
commit 8db50bdc90
5 changed files with 143 additions and 65 deletions

View File

@ -27,4 +27,4 @@ auto-deploy:
services: services:
- docker:dind - docker:dind
script: script:
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-berita-bumn/build?token=autodeploymedols - curl --user admin:$JENKINS_PWD http:// /job/auto-deploy-berita-bumn/build?token=autodeploymedols

View File

@ -5,6 +5,7 @@ import Link from "next/link";
import { import {
getArticleById, getArticleById,
getArticleBySlug, getArticleBySlug,
getArticleFiles,
getListArticle, getListArticle,
} from "@/service/article"; } from "@/service/article";
import { close, error, loading } from "@/config/swal"; import { close, error, loading } from "@/config/swal";
@ -74,7 +75,7 @@ export default function DetailContent() {
startDate: null, startDate: null,
endDate: null, endDate: null,
}); });
const [detailfiles, setDetailFiles] = useState<any>([]); const [detailFiles, setDetailFiles] = useState<any[]>([]);
const [mainImage, setMainImage] = useState(0); const [mainImage, setMainImage] = useState(0);
const [thumbnail, setThumbnail] = useState("-"); const [thumbnail, setThumbnail] = useState("-");
const [diseId, setDiseId] = useState(0); const [diseId, setDiseId] = useState(0);
@ -300,16 +301,38 @@ export default function DetailContent() {
initStateData(); initStateData();
}, [listCategory]); }, [listCategory]);
useEffect(() => {
setSelectedIndex(0);
}, [detailFiles]);
async function initStateData() { async function initStateData() {
loading(); loading();
const res = await getArticleBySlug(slug); try {
const data = res.data?.data; // 1⃣ Ambil artikel by slug
const res = await getArticleBySlug(slug);
const data = res?.data?.data;
setThumbnail(data?.thumbnailUrl); if (!data?.id) return;
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files); setArticleDetail(data);
setArticleDetail(data); setThumbnail(data.thumbnailUrl);
close(); setDiseId(data.aiArticleId);
// 2⃣ Ambil SEMUA article files
const filesRes = await getArticleFiles();
const allFiles = filesRes?.data?.data ?? [];
// 3⃣ FILTER sesuai articleId
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === data.id
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state detail error:", error);
} finally {
close();
}
} }
if (!articleDetail?.files || articleDetail.files.length === 0) { if (!articleDetail?.files || articleDetail.files.length === 0) {
@ -320,6 +343,23 @@ export default function DetailContent() {
); );
} }
function decodeHtmlString(raw: string = "") {
if (!raw) return "";
// 1⃣ Hapus newline escape, backslash, dsb
let decoded = raw
.replace(/\\n/g, "\n")
.replace(/\\"/g, '"') // ubah \" jadi "
.replace(/\\'/g, "'") // ubah \' jadi '
.replace(/\\\\/g, "\\") // ubah \\ jadi \
.trim();
// 2⃣ Decode entity HTML (misal &quot;)
const el = document.createElement("textarea");
el.innerHTML = decoded;
return el.value;
}
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">
@ -370,38 +410,43 @@ export default function DetailContent() {
<div className="w-full h-auto mb-6"> <div className="w-full h-auto mb-6">
{/* Gambar utama */} {/* Gambar utama */}
<div className="w-full"> {detailFiles.length > 0 ? (
<Image <>
src={articleDetail.files[selectedIndex].fileUrl} <Image
alt={articleDetail.files[selectedIndex].fileAlt || "Berita"} src={detailFiles[selectedIndex]?.fileUrl}
width={800} alt={detailFiles[selectedIndex]?.fileAlt || "Berita"}
height={400} width={800}
className="rounded-lg w-full object-cover" height={400}
/> className="rounded-lg w-full object-cover"
</div> />
{/* Thumbnail */} <div className="flex gap-2 mt-3 overflow-x-auto">
<div className="flex gap-2 mt-3 overflow-x-auto"> {detailFiles.map((file, index) => (
{articleDetail.files.map((file: any, index: number) => ( <button
<button key={file.id}
key={file.id || index} onClick={() => setSelectedIndex(index)}
onClick={() => setSelectedIndex(index)} className={`border-2 rounded-lg ${
className={`border-2 rounded-lg overflow-hidden ${ selectedIndex === index
selectedIndex === index ? "border-red-500"
? "border-red-500" : "border-transparent"
: "border-transparent" }`}
}`} >
> <Image
<Image src={file.fileUrl}
src={file.fileUrl} alt={file.fileAlt || "Thumbnail"}
alt={file.fileAlt || "Thumbnail"} width={100}
width={100} height={80}
height={80} className="object-cover"
className="object-cover" />
/> </button>
</button> ))}
))} </div>
</div> </>
) : (
<div className="h-[400px] flex items-center justify-center bg-gray-100 rounded-lg">
<p className="text-gray-400">Gambar tidak tersedia</p>
</div>
)}
{/* Slug */} {/* Slug */}
<p className="text-sm text-gray-500 mt-2 text-end"> <p className="text-sm text-gray-500 mt-2 text-end">
@ -475,10 +520,12 @@ export default function DetailContent() {
</Link> </Link>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<div className="text-gray-700 leading-relaxed text-justify"> <div className="prose max-w-none text-justify">
<div <div
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: articleDetail?.htmlDescription || "", __html: decodeHtmlString(
articleDetail?.htmlDescription || ""
),
}} }}
/> />
</div> </div>

View File

@ -514,7 +514,7 @@ export default function CreateArticleForm() {
id="title" id="title"
type="text" type="text"
placeholder="Masukkan judul artikel" placeholder="Masukkan judul artikel"
className="w-full border rounded-lg dark:border-gray-400" className="h-16 px-4 text-2xl leading-tight"
{...field} {...field}
/> />
)} )}

View File

@ -23,6 +23,7 @@ import {
deleteArticleFiles, deleteArticleFiles,
getArticleByCategory, getArticleByCategory,
getArticleById, getArticleById,
getArticleFiles,
submitApproval, submitApproval,
unPublishArticle, unPublishArticle,
updateArticle, updateArticle,
@ -196,27 +197,49 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
async function initState() { async function initState() {
loading(); loading();
const res = await getArticleById(id); try {
const data = res.data?.data; // 1⃣ Ambil ARTICLE
setDetailData(data); const articleRes = await getArticleById(id);
setValue("title", data?.title); const articleData = articleRes.data?.data;
setValue("customCreatorName", data?.customCreatorName);
setValue("slug", data?.slug);
setValue("source", data?.source);
const cleanDescription = data?.htmlDescription
? data.htmlDescription
.replace(/\\"/g, '"')
.replace(/\\n/g, "\n", "\\")
.trim()
: "";
setValue("description", cleanDescription);
setValue("tags", data?.tags ? data.tags.split(",") : []);
setThumbnail(data?.thumbnailUrl);
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files);
setupInitCategory(data?.categories); if (!articleData) return;
close();
// ===== ARTICLE DATA =====
setDetailData(articleData);
setValue("title", articleData.title);
setValue("customCreatorName", articleData.customCreatorName);
setValue("slug", articleData.slug);
setValue("source", articleData.source);
const cleanDescription = articleData.htmlDescription
? articleData.htmlDescription
.replace(/\\"/g, '"')
.replace(/\\n/g, "\n")
.trim()
: "";
setValue("description", cleanDescription);
setValue("tags", articleData.tags ? articleData.tags.split(",") : []);
setThumbnail(articleData.thumbnailUrl);
setDiseId(articleData.aiArticleId);
setupInitCategory(articleData.categories);
// 2⃣ Ambil SEMUA article files
const filesRes = await getArticleFiles();
const allFiles = filesRes.data?.data ?? [];
// 3⃣ FILTER berdasarkan ARTICLE ID yang sedang dibuka
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === articleData.id
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state error:", error);
} finally {
close();
}
} }
const setupInitCategory = (data: any) => { const setupInitCategory = (data: any) => {
@ -667,9 +690,10 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
name="title" name="title"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<div className="w-full"> <div className="w-full">
<label htmlFor="title" className="block text-sm font-medium mb-1"> <label htmlFor="title" className="block text-xl font-medium mb-2">
Judul Judul
</label> </label>
<Input <Input
type="text" type="text"
id="title" id="title"
@ -677,7 +701,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
value={value ?? ""} value={value ?? ""}
readOnly={isDetail} readOnly={isDetail}
onChange={onChange} onChange={onChange}
className="w-full border rounded-lg" className="h-16 px-4 text-2xl leading-tight"
/> />
</div> </div>
)} )}

View File

@ -143,6 +143,13 @@ export async function uploadArticleThumbnail(id: string, data: any) {
return await httpPostInterceptor(`/articles/thumbnail/${id}`, data, headers); return await httpPostInterceptor(`/articles/thumbnail/${id}`, data, headers);
} }
export async function getArticleFiles() {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/article-files`, headers);
}
export async function deleteArticleFiles(id: number) { export async function deleteArticleFiles(id: number) {
const headers = { const headers = {
"content-type": "multipart/form-data", "content-type": "multipart/form-data",