fix: all detail content

This commit is contained in:
Sabda Yagra 2025-10-19 23:07:42 +07:00
parent 1ce44acc3c
commit ef4887cfc1
8 changed files with 255 additions and 237 deletions

View File

@ -1,158 +1,152 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { Calendar, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import {
FaFacebookF,
FaTiktok,
FaYoutube,
FaWhatsapp,
FaInstagram,
FaTwitter,
FaCheck,
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail } from "@/service/landing/landing";
import { BarWave } from "react-cssfx-loading";
import WaveSurfer from "wavesurfer.js";
import { getArticleDetail } from "@/service/content/content";
export default function AudioDetail({ id }: { id: number }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
const [showShareMenu, setShowShareMenu] = useState(false);
const [audioLoaded, setAudioLoaded] = useState(false);
const [playing, setPlaying] = useState(false);
const [volume, setVolume] = useState(0.5);
const waveformRef = useRef<HTMLDivElement>(null);
const wavesurfer = useRef<any>(null);
const [selectedAudio, setSelectedAudio] = useState(0);
// Salin tautan
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Gagal menyalin link:", err);
}
};
const SocialItem = ({
icon,
label,
}: {
icon: React.ReactNode;
label: string;
}) => (
<div className="flex items-center gap-3 cursor-pointer hover:opacity-80">
<div className="bg-[#C6A455] p-2 rounded-full text-white">{icon}</div>
<span className="text-sm">{label}</span>
</div>
);
// Ambil data audio dari API lama
useEffect(() => {
const fetchDetail = async () => {
try {
setLoading(true);
// 1⃣ Coba API baru
const res = await getArticleDetail(id);
console.log("Audio detail API response:", res);
const article = res?.data?.data;
if (article) {
const mappedData = {
id: article.id,
title: article.title,
description: article.description,
createdAt: article.createdAt,
clickCount: article.viewCount,
creatorGroupLevelName: article.createdByName || "Unknown",
uploadedBy: {
publisher: article.createdByName || "MABES POLRI",
},
files:
article.files?.map((f: any) => ({
id: f.id,
fileName: f.file_name,
url: f.file_url,
secondaryUrl: f.file_url,
fileAlt: f.file_alt,
size: f.size,
createdAt: f.created_at,
updatedAt: f.updated_at,
})) || [],
};
setData(mappedData);
return;
}
// 2⃣ Fallback ke API lama
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
} catch (err) {
console.error("Gagal memuat audio:", err);
// try {
// const fallback = await getDetail(id);
// setData(fallback?.data?.data);
// } catch (err2) {
// console.error("Fallback gagal:", err2);
// }
} finally {
setLoading(false);
}
const fetchData = async () => {
setLoading(true);
const res = await getArticleDetail(id);
const detail = res?.data?.data;
if (detail) setData(detail);
setLoading(false);
};
if (id) fetchDetail();
fetchData();
}, [id]);
if (loading) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
<p>Memuat data audio...</p>
</div>
);
}
// Inisialisasi WaveSurfer
useEffect(() => {
if (!data?.files?.[selectedAudio]?.url) return;
if (!data) {
return (
<div className="max-w-6xl mx-auto px-4 py-6">
<p>Data tidak ditemukan</p>
</div>
);
}
// Hancurkan instance sebelumnya jika ada
if (wavesurfer.current) wavesurfer.current.destroy();
const url = data.files[selectedAudio].url;
const options = {
container: waveformRef.current!,
waveColor: "#E0E0E0",
progressColor: "#FFC831",
cursorColor: "#000",
barWidth: 2,
barRadius: 2,
responsive: true,
height: 80,
normalize: true,
partialRender: true,
};
const ws = WaveSurfer.create(options);
wavesurfer.current = ws;
ws.load(url);
ws.on("ready", () => {
setAudioLoaded(true);
ws.setVolume(volume);
});
ws.on("finish", () => {
setPlaying(false);
});
return () => {
ws.destroy();
};
}, [data?.files, selectedAudio]);
const handlePlayPause = () => {
if (!wavesurfer.current) return;
wavesurfer.current.playPause();
setPlaying(!playing);
};
const handleVolumeChange = (e: any) => {
const vol = parseFloat(e.target.value);
setVolume(vol);
wavesurfer.current?.setVolume(vol);
};
if (loading) return <p className="p-6">Memuat audio...</p>;
if (!data) return <p className="p-6">Data tidak ditemukan</p>;
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Pemutar Audio Utama */}
<div className="relative">
<audio
controls
className="w-full"
src={data?.files?.[selectedAudio]?.url || ""}
>
Browser Anda tidak mendukung elemen audio.
</audio>
<div className="max-w-5xl mx-auto px-4 py-6 space-y-6">
{/* 🎵 Player */}
<div className="relative flex flex-col items-start">
<div className="flex items-center gap-4">
<button
onClick={handlePlayPause}
className="w-12 h-12 bg-yellow-500 rounded-full flex justify-center items-center text-white text-xl hover:bg-yellow-600"
>
{playing ? "❚❚" : "▶"}
</button>
{/* Loading sebelum waveform siap */}
{!audioLoaded && (
<BarWave
color="#ffc831"
width="60px"
height="25px"
duration="2s"
className="ml-5"
/>
)}
{/* Waveform */}
<div ref={waveformRef} className="w-[300px] sm:w-[600px] ml-4"></div>
</div>
{/* Volume control */}
{audioLoaded && (
<div className="flex items-center gap-2 mt-2">
<span>🔊</span>
<input
type="range"
min="0"
max="1"
step="0.05"
value={volume}
onChange={handleVolumeChange}
/>
</div>
)}
</div>
{/* Pilihan file audio */}
<div className="py-4 px-1 flex flex-row gap-3 flex-wrap">
{/* 🔸 Daftar File Audio */}
<div className="py-4 flex flex-row gap-3 flex-wrap">
{data?.files?.map((file: any, index: number) => (
<button
key={file?.id}
onClick={() => setSelectedAudio(index)}
className={`px-4 py-2 rounded-md border cursor-pointer hover:bg-gray-100 ${
selectedAudio === index ? "border-red-600 bg-gray-50" : ""
selectedAudio === index ? "border-yellow-600 bg-yellow-50" : ""
}`}
>
🎵 {file?.fileName || `Audio ${index + 1}`}
🎧 {file?.fileName || `Audio ${index + 1}`}
</button>
))}
</div>
{/* Informasi artikel */}
{/* 🗓️ Informasi Artikel */}
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span className="font-semibold text-black border-r-2 pr-2 border-black">
by {data?.uploadedBy?.publisher || "MABES POLRI"}
by {data?.uploadedBy?.userLevel?.name || "MABES POLRI"}
</span>
<span className="flex items-center gap-1 text-black">
<Calendar className="w-4 h-4" />
@ -173,65 +167,16 @@ export default function AudioDetail({ id }: { id: number }) {
<Eye className="w-4 h-4" />
{data.clickCount || 0}
</span>
<span className="text-black">
Creator: {data.creatorGroupLevelName}
</span>
<span className="text-black">Creator: {data.creatorName}</span>
</div>
{/* Deskripsi */}
<div className="flex flex-col md:flex-row gap-6 mt-6">
<div className="flex-1 space-y-4">
<h1 className="text-xl font-bold">{data.title}</h1>
<div className="text-base text-gray-700 leading-relaxed space-y-3">
<p>{data.description}</p>
</div>
{/* Tombol aksi bawah */}
<div className="flex flex-wrap justify-center gap-4 my-20">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
size="lg"
className="justify-start bg-black text-white rounded-full"
>
{copied ? <FaCheck /> : <FaLink />}
</Button>
<span>COPY LINK</span>
</div>
<div className="flex gap-2 items-center relative">
<Button
onClick={() => setShowShareMenu(!showShareMenu)}
size="lg"
className="justify-start bg-[#C6A455] text-white rounded-full"
>
<FaShareAlt />
</Button>
<span>SHARE</span>
{showShareMenu && (
<div className="absolute left-16 top-0 bg-white p-4 rounded-lg shadow-lg flex flex-col gap-3 w-48 z-10">
<SocialItem icon={<FaFacebookF />} label="Facebook" />
<SocialItem icon={<FaTiktok />} label="TikTok" />
<SocialItem icon={<FaYoutube />} label="YouTube" />
<SocialItem icon={<FaWhatsapp />} label="WhatsApp" />
<SocialItem icon={<FaInstagram />} label="Instagram" />
<SocialItem icon={<FaTwitter />} label="Twitter" />
</div>
)}
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/audio/comment/${id}`}>
<Button
variant="default"
size="lg"
className="justify-start bg-[#FFAD10] rounded-full mr-2 text-white"
>
💬
</Button>
COMMENT
</Link>
</div>
</div>
</div>
{/* 📝 Deskripsi */}
<div className="mt-4">
<h1 className="text-xl font-bold">{data.title}</h1>
<div
className="text-base text-gray-700 leading-relaxed space-y-3 mt-2"
dangerouslySetInnerHTML={{ __html: data.htmlDescription }}
/>
</div>
</div>
);

View File

@ -70,14 +70,26 @@ export default function DocumentDetail({ id }: { id: number }) {
uploadedBy: {
publisher: article.createdByName || "MABES POLRI",
},
// files:
// article.files?.map((f: any) => ({
// id: f.id,
// fileName: f.file_name,
// url: f.file_url,
// secondaryUrl: f.file_url,
// fileThumbnail: f.file_thumbnail,
// fileAlt: f.file_alt,
// size: f.size,
// createdAt: f.created_at,
// updatedAt: f.updated_at,
// })) || [],
files:
article.files?.map((f: any) => ({
id: f.id,
fileName: f.file_name,
url: f.file_url,
secondaryUrl: f.file_url,
fileThumbnail: f.file_thumbnail,
fileAlt: f.file_alt,
fileName: f.fileName || f.file_name,
url: f.fileUrl || f.file_url,
secondaryUrl: f.fileUrl || f.file_url, // 🟢 gunakan field yang benar
fileThumbnail: f.fileThumbnail || f.file_thumbnail,
fileAlt: f.fileAlt || f.file_alt,
size: f.size,
createdAt: f.created_at,
updatedAt: f.updated_at,
@ -125,13 +137,45 @@ export default function DocumentDetail({ id }: { id: number }) {
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Viewer dokumen utama */}
{/* <div className="relative">
<iframe
src={data?.files?.[selectedDoc]?.secondaryUrl || ""}
className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full"
/>
</div> */}
<div className="bg-[#e0c350] flex items-center justify-center h-[170px] text-white">
<div className="relative">
{data?.files?.[selectedDoc]?.secondaryUrl ? (
<>
{console.log(
"📄 File URL:",
data?.files?.[selectedDoc]?.secondaryUrl
)}
<iframe
key={selectedDoc}
src={`https://docs.google.com/gview?url=${encodeURIComponent(
data?.files?.[selectedDoc]?.secondaryUrl || ""
)}&embedded=true`}
className="rounded-lg w-full h-[600px] border"
onError={(e) => {
console.error("❌ Gagal load iframe:", e);
window.open(data?.files?.[selectedDoc]?.secondaryUrl, "_blank");
}}
/>
<p className="text-center text-xs text-gray-500 mt-2">
Jika preview tidak muncul,{" "}
<a
href={data?.files?.[selectedDoc]?.secondaryUrl}
target="_blank"
className="text-blue-600 underline"
>
klik di sini untuk buka file
</a>
</p>
</>
) : (
<div className="w-full h-[300px] flex items-center justify-center bg-gray-100 text-gray-500 rounded-lg">
Tidak ada file untuk ditampilkan.
</div>
)}
</div>
{/* <div className="bg-[#e0c350] flex items-center justify-center h-[170px] text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
width="150"
@ -143,7 +187,7 @@ export default function DocumentDetail({ id }: { id: number }) {
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207zM7 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M7.5 9a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM7 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M5.5 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1M6 9.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0M5.5 12a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1"
/>
</svg>
</div>
</div> */}
{/* Pilihan dokumen */}
<div className="py-4 px-1 flex flex-row gap-3 flex-wrap">

View File

@ -160,9 +160,11 @@ export default function ImageDetail({ id }: { id: number }) {
width={2560}
height={1440}
src={
data?.files?.[selectedImage]?.url ||
data?.thumbnailUrl || // ✅ fallback ke thumbnailUrl
"/nodata.png"
data?.files?.[selectedImage]?.url?.trim()
? data.files[selectedImage].url
: data?.thumbnailUrl?.trim()
? data.thumbnailUrl
: "/assets/empty-data.png"
}
alt="Main"
className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full object-contain"

View File

@ -52,18 +52,18 @@ export default function VideoDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
setLoading(true);
// Try new Articles API first
const response = await getArticleDetail(id);
console.log("Article Detail API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
return;
}
// if (response?.error) {
// console.error("Articles API failed, falling back to old API");
// // Fallback to old API
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
// return;
// }
// Handle new API response structure
const articleData = response?.data?.data;
@ -77,35 +77,37 @@ export default function VideoDetail({ id }: { id: string }) {
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
publisher: articleData.createdByName || "MABES POLRI",
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
thumbnailFileUrl: file.file_thumbnail || articleData.thumbnailUrl,
...file
})) || [],
...articleData
files:
articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
thumbnailFileUrl:
file.file_thumbnail || articleData.thumbnailUrl,
...file,
})) || [],
...articleData,
};
setData(transformedData);
}
} catch (error) {
console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
// const fallbackResponse = await getDetail(id);
// setData(fallbackResponse?.data?.data);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
@ -134,28 +136,48 @@ export default function VideoDetail({ id }: { id: string }) {
return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Bagian Video Utama */}
<div className="relative max-h-screen overflow-hidden">
<div className="w-full max-h-screen aspect-video">
<div className="w-full h-full object-contain">
<VideoPlayer url={data?.files[selectedVideo]?.url} />
<VideoPlayer
url={
// data?.files?.[selectedVideo]?.secondaryUrl?.trim()
// ? data.files[selectedVideo].secondaryUrl
// : data?.files?.[selectedVideo]?.fileUrl?.includes("viewer/")
// ? `${process.env.NEXT_PUBLIC_API}/media/view?id=${data.files[selectedVideo].id}&operation=file&type=video`
// : data?.files?.[selectedVideo]?.fileUrl?.trim()
// ?
data.files[selectedVideo].fileUrl
// : "/notfound.mp4"
}
/>
</div>
</div>
<div className="absolute top-4 left-4"></div>
</div>
<div className="py-2 flex flex-row gap-3">
{/* Thumbnail bawah */}
<div className="py-2 flex flex-row gap-3 flex-wrap">
{data?.files?.map((file: any, index: number) => (
<div
key={file?.id}
onClick={() => setSelectedVideo(index)}
className="cursor-pointer flex flex-col items-center gap-1"
>
<img
src={file?.thumbnailFileUrl}
alt={file?.fileName}
className="w-32 h-20 object-cover rounded"
<Image
src={
file?.thumbnailFileUrl?.trim()
? file.thumbnailFileUrl
: "/notfound.png"
}
alt={file?.fileName || "thumbnail"}
width={160}
height={90}
className={`w-32 h-20 object-cover rounded-md hover:ring-2 ${
selectedVideo === index ? "ring-2 ring-red-600" : ""
}`}
/>
{/* <p className="text-sm text-center">{file?.fileName}</p> */}
</div>
))}
</div>
@ -197,7 +219,7 @@ export default function VideoDetail({ id }: { id: string }) {
<div className="flex flex-col md:flex-row gap-6 mt-6">
{/* Sidebar actions */}
<div className="hidden md:flex flex-col gap-4 relative z-10">
{/* <div className="hidden md:flex flex-col gap-4 relative z-10">
<div className="flex gap-2 items-center">
<Button
onClick={handleCopyLink}
@ -253,7 +275,7 @@ export default function VideoDetail({ id }: { id: string }) {
COMMENT
</Link>
</div>
</div>
</div> */}
{/* Content */}
<div className="flex-1 space-y-4">

10
package-lock.json generated
View File

@ -133,6 +133,7 @@
"tus-js-client": "^4.3.1",
"use-places-autocomplete": "^4.0.1",
"uuid": "^13.0.0",
"wavesurfer.js": "^7.11.0",
"yup": "^1.6.1",
"zod": "^3.23.8"
},
@ -18923,6 +18924,8 @@
},
"node_modules/react-cssfx-loading": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-cssfx-loading/-/react-cssfx-loading-2.1.0.tgz",
"integrity": "sha512-0SnS6HpaeLSaTxNuND6sAKTQmoKgjwFb9G2ltyEMmA5ARNN6TRQfiJ8PfaYM9RwVEOhDxIzGI7whb2zeI1VRxw==",
"license": "MIT",
"dependencies": {
"goober": "^2.1.10"
@ -22558,9 +22561,10 @@
}
},
"node_modules/wavesurfer.js": {
"version": "7.8.4",
"license": "BSD-3-Clause",
"peer": true
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.11.0.tgz",
"integrity": "sha512-LOGdIBIKv/roYuQYClhoqhwbIdQL1GfobLnS2vx0heoLD9lu57OUHWE2DIsCNXBvCsmmbkUvJq9W8bPLPbikGw==",
"license": "BSD-3-Clause"
},
"node_modules/wcwidth": {
"version": "1.0.1",

View File

@ -134,6 +134,7 @@
"tus-js-client": "^4.3.1",
"use-places-autocomplete": "^4.0.1",
"uuid": "^13.0.0",
"wavesurfer.js": "^7.11.0",
"yup": "^1.6.1",
"zod": "^3.23.8"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -30,7 +30,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ url }) => {
controls
playing
muted
pip={false} //
pip={false}
config={{
file: {
attributes: {