feat(berita): add multiple images gallery and YouTube video support

- Update schema: add images relation list and linkVideo field
- API: support multiple image upload and YouTube link in create/update
- Admin create page: add gallery upload (max 10) and YouTube embed preview
- Admin edit page: manage existing/new gallery images and YouTube link
- Admin detail page: display gallery grid and YouTube video embed
- Public detail page: show gallery images and YouTube video with responsive layout
- State: add imageIds[] and linkVideo fields with proper type handling
- Music player: fix seek functionality and ESLint warnings

Breaking changes:
- Prisma schema updated - requires migration
- API create/update endpoints now expect imageIds array and linkVideo string

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-02 16:06:53 +08:00
parent ae3187804e
commit a5bd91b580
12 changed files with 804 additions and 213 deletions

View File

@@ -8,8 +8,7 @@ export function useAudioProgress(
audioRef: React.RefObject<HTMLAudioElement>,
isPlaying: boolean,
setCurrentTime: (time: number) => void,
isSeekingRef: React.RefObject<boolean>,
lastSeekTimeRef?: React.RefObject<number>
isSeekingRef: React.RefObject<boolean>
) {
const rafRef = useRef<number | null>(null);
const lastTimeRef = useRef<number>(0);

View File

@@ -50,7 +50,7 @@ const MusicPlayer = () => {
const lastSeekTimeRef = useRef<number>(0); // Track last seek time
// Smooth progress update dengan requestAnimationFrame
useAudioProgress(audioRef as React.RefObject<HTMLAudioElement>, isPlaying, setCurrentTime, isSeekingRef, lastSeekTimeRef);
useAudioProgress(audioRef as React.RefObject<HTMLAudioElement>, isPlaying, setCurrentTime, isSeekingRef);
// Fetch musik data from API
useEffect(() => {
@@ -108,7 +108,6 @@ const MusicPlayer = () => {
// }, [isPlaying]);
// Update duration when song changes (HANYA saat ganti lagu, bukan saat isPlaying berubah)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (currentSong && audioRef.current) {
// Cek apakah ini benar-benar lagu baru
@@ -136,10 +135,9 @@ const MusicPlayer = () => {
}
// Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0)
}
}, [currentSong?.id]);
}, [currentSong?.id]); // Intentional: hanya depend on song ID, bukan isPlaying
// Sync duration dari audio element jika berbeda signifikan (> 1 detik)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
const audio = audioRef.current;
if (!audio || !currentSong) return;
@@ -157,7 +155,7 @@ const MusicPlayer = () => {
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
}, [currentSong?.id]);
}, [currentSong?.id]); // Intentional: hanya depend on song ID
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);