This commit is contained in:
Anang Yusman 2026-01-14 12:04:14 +08:00
parent 07316b0aa8
commit 439cf1b665
5 changed files with 157 additions and 67 deletions

View File

@ -5,6 +5,7 @@ import Link from "next/link";
import {
getArticleById,
getArticleBySlug,
getArticleFiles,
getListArticle,
} from "@/service/article";
import { close, error, loading } from "@/config/swal";
@ -74,7 +75,7 @@ export default function DetailContent() {
startDate: null,
endDate: null,
});
const [detailfiles, setDetailFiles] = useState<any>([]);
const [detailFiles, setDetailFiles] = useState<any[]>([]);
const [mainImage, setMainImage] = useState(0);
const [thumbnail, setThumbnail] = useState("-");
const [diseId, setDiseId] = useState(0);
@ -257,16 +258,55 @@ export default function DetailContent() {
initStateData();
}, [listCategory]);
useEffect(() => {
setSelectedIndex(0);
}, [detailFiles]);
async function initStateData() {
loading();
const res = await getArticleBySlug(slug);
const data = res?.data?.data;
try {
// 1⃣ Ambil artikel by slug
const res = await getArticleBySlug(slug);
const data = res?.data?.data;
setThumbnail(data?.thumbnailUrl);
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files);
setArticleDetail(data); // <-- Add this
close();
if (!data?.id) return;
setArticleDetail(data);
setThumbnail(data.thumbnailUrl);
setDiseId(data.aiArticleId);
// 2⃣ Ambil SEMUA article files
const filesRes = await getArticleFiles();
const allFiles = filesRes?.data?.data ?? [];
// 3⃣ FILTER sesuai articleId
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === data.id
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state detail error:", error);
} finally {
close();
}
}
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 (
@ -324,38 +364,43 @@ export default function DetailContent() {
</div>
<div className="w-full h-auto mb-6">
<div className="w-full">
<Image
src={articleDetail?.files[selectedIndex].fileUrl}
alt={articleDetail?.files[selectedIndex].fileAlt || "Berita"}
width={800}
height={400}
className="rounded-lg w-full object-cover"
/>
</div>
{detailFiles.length > 0 ? (
<>
<Image
src={detailFiles[selectedIndex]?.fileUrl}
alt={detailFiles[selectedIndex]?.fileAlt || "Berita"}
width={800}
height={400}
className="rounded-lg w-full object-cover"
/>
{/* Thumbnail */}
<div className="flex gap-2 mt-3 overflow-x-auto">
{articleDetail?.files.map((file: any, index: number) => (
<button
key={file.id || index}
onClick={() => setSelectedIndex(index)}
className={`border-2 rounded-lg overflow-hidden ${
selectedIndex === index
? "border-red-500"
: "border-transparent"
}`}
>
<Image
src={file.fileUrl}
alt={file.fileAlt || "Thumbnail"}
width={100}
height={80}
className="object-cover"
/>
</button>
))}
</div>
<div className="flex gap-2 mt-3 overflow-x-auto">
{detailFiles.map((file, index) => (
<button
key={file.id}
onClick={() => setSelectedIndex(index)}
className={`border-2 rounded-lg ${
selectedIndex === index
? "border-red-500"
: "border-transparent"
}`}
>
<Image
src={file.fileUrl}
alt={file.fileAlt || "Thumbnail"}
width={100}
height={80}
className="object-cover"
/>
</button>
))}
</div>
</>
) : (
<div className="h-[400px] flex items-center justify-center bg-gray-100 rounded-lg">
<p className="text-gray-400">Gambar tidak tersedia</p>
</div>
)}
<div className=" flex flex-row w-fit rounded overflow-hidden mr-5 gap-3 mt-3">
<div className="flex flex-col items-center gap-2">

View File

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

View File

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

View File

@ -129,6 +129,13 @@ export async function getCategoryPagination(data: any) {
);
}
export async function getArticleFiles() {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/article-files`, headers);
}
export async function uploadArticleFile(id: string, data: any) {
const headers = {
"content-type": "multipart/form-data",

View File

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