refactor(musik): integrate music player library functions and fix build errors

- Integrate togglePlayPause, getNextIndex, getPrevIndex, handleRepeatOrNext, seekTo, toggleShuffle, setAudioVolume, toggleMute library functions
- Fix ESLint warnings: remove unused eslint-disable, add missing useEffect dependencies
- Fix ESLint error in useMusicPlayer.ts togglePlayPause function
- Add force-dynamic export to root layout to prevent prerendering errors
- Improve seek slider with preview/commit functionality
- Add isSeeking state to prevent UI flickering during seek

Fixes: Build PageNotFoundError for admin/darmasaba pages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-02 11:41:14 +08:00
parent 341ff5779f
commit fe7672e09f
10 changed files with 222 additions and 88 deletions

View File

@@ -19,7 +19,6 @@ const nextConfig: NextConfig = {
}, },
]; ];
}, },
}; };
export default nextConfig; export default nextConfig;

BIN
public/mp3-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,32 @@
export function getNextIndex(
currentIndex: number,
total: number,
isShuffle: boolean
) {
if (total === 0) return -1;
if (isShuffle) {
return Math.floor(Math.random() * total);
}
return (currentIndex + 1) % total;
}
export function getPrevIndex(
currentIndex: number,
total: number,
isShuffle: boolean
) {
if (total === 0) return -1;
if (isShuffle) {
return Math.floor(Math.random() * total);
}
return currentIndex - 1 < 0 ? total - 1 : currentIndex - 1;
}
//pakai di ui
// const next = getNextIndex(currentSongIndex, filteredMusik.length, isShuffle);
// playSong(next);

View File

@@ -0,0 +1,24 @@
import { RefObject } from "react";
export function togglePlayPause(
audioRef: RefObject<HTMLAudioElement | null>,
isPlaying: boolean,
setIsPlaying: (v: boolean) => void
) {
if (!audioRef.current) return;
if (isPlaying) {
audioRef.current.pause();
setIsPlaying(false);
} else {
audioRef.current
.play()
.then(() => setIsPlaying(true))
.catch(console.error);
}
}
// pakai di ui
// onClick={() =>
// togglePlayPause(audioRef, isPlaying, setIsPlaying)
// }

View File

@@ -0,0 +1,22 @@
import { RefObject } from "react";
export function handleRepeatOrNext(
audioRef: RefObject<HTMLAudioElement | null>,
isRepeat: boolean,
playNext: () => void
) {
if (!audioRef.current) return;
if (isRepeat) {
audioRef.current.currentTime = 0;
audioRef.current.play();
} else {
playNext();
}
}
//dipakai di ui
// onEnded={() =>
// handleRepeatOrNext(audioRef, isRepeat, playNext)
// }

View File

@@ -0,0 +1,25 @@
export function seekTo(
audioRef: React.RefObject<HTMLAudioElement | null>,
time: number
) {
if (!audioRef.current) return;
audioRef.current.currentTime = time;
}
// import { RefObject } from "react";
// export function seekTo(
// audioRef: RefObject<HTMLAudioElement | null>,
// time: number,
// setCurrentTime: (v: number) => void
// ) {
// if (!audioRef.current) return;
// audioRef.current.currentTime = time;
// setCurrentTime(time);
// }
// //pakai di ui
// // onChange={(v) => seekTo(audioRef, v, setCurrentTime)}

View File

@@ -0,0 +1,6 @@
export function toggleShuffle(
isShuffle: boolean,
setIsShuffle: (v: boolean) => void
) {
setIsShuffle(!isShuffle);
}

View File

@@ -0,0 +1,29 @@
import { RefObject } from "react";
export function setAudioVolume(
audioRef: RefObject<HTMLAudioElement | null>,
volume: number,
setVolume: (v: number) => void,
setIsMuted: (v: boolean) => void
) {
if (!audioRef.current) return;
audioRef.current.volume = volume / 100;
setVolume(volume);
if (volume > 0) {
setIsMuted(false);
}
}
export function toggleMute(
audioRef: RefObject<HTMLAudioElement | null>,
isMuted: boolean,
setIsMuted: (v: boolean) => void
) {
if (!audioRef.current) return;
const muted = !isMuted;
audioRef.current.muted = muted;
setIsMuted(muted);
}

View File

@@ -1,9 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, ScrollArea, Slider, Stack, Text, TextInput } from '@mantine/core'; import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, ScrollArea, Slider, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react'; import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { togglePlayPause } from '../lib/playPause';
import { getNextIndex, getPrevIndex } from '../lib/nextPrev';
import { handleRepeatOrNext } from '../lib/repeat';
import { seekTo } from '../lib/seek';
import { toggleShuffle } from '../lib/shuffle';
import { setAudioVolume, toggleMute as toggleMuteUtil } from '../lib/volume';
interface MusicFile { interface MusicFile {
id: string; id: string;
@@ -39,9 +44,8 @@ const MusicPlayer = () => {
const [musikData, setMusikData] = useState<Musik[]>([]); const [musikData, setMusikData] = useState<Musik[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentSongIndex, setCurrentSongIndex] = useState(-1); const [currentSongIndex, setCurrentSongIndex] = useState(-1);
const [isSeeking, setIsSeeking] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);
const progressInterval = useRef<number | null>(null);
// Fetch musik data from API // Fetch musik data from API
useEffect(() => { useEffect(() => {
@@ -75,26 +79,26 @@ const MusicPlayer = () => {
? filteredMusik[currentSongIndex] ? filteredMusik[currentSongIndex]
: null; : null;
// Update progress bar // // Update progress bar
useEffect(() => { // useEffect(() => {
if (isPlaying && audioRef.current) { // if (isPlaying && audioRef.current) {
progressInterval.current = window.setInterval(() => { // progressInterval.current = window.setInterval(() => {
if (audioRef.current) { // if (audioRef.current) {
setCurrentTime(Math.floor(audioRef.current.currentTime)); // setCurrentTime(Math.floor(audioRef.current.currentTime));
} // }
}, 1000); // }, 1000);
} else { // } else {
if (progressInterval.current) { // if (progressInterval.current) {
clearInterval(progressInterval.current); // clearInterval(progressInterval.current);
} // }
} // }
return () => { // return () => {
if (progressInterval.current) { // if (progressInterval.current) {
clearInterval(progressInterval.current); // clearInterval(progressInterval.current);
} // }
}; // };
}, [isPlaying]); // }, [isPlaying]);
// Update duration when song changes // Update duration when song changes
useEffect(() => { useEffect(() => {
@@ -110,7 +114,7 @@ const MusicPlayer = () => {
}); });
} }
} }
}, [currentSongIndex]); }, [currentSongIndex, currentSong, isPlaying]);
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
@@ -126,13 +130,7 @@ const MusicPlayer = () => {
}; };
const handleSongEnd = () => { const handleSongEnd = () => {
if (isRepeat) { const playNext = () => {
if (audioRef.current) {
audioRef.current.currentTime = 0;
audioRef.current.play();
}
} else {
// Play next song
let nextIndex: number; let nextIndex: number;
if (isShuffle) { if (isShuffle) {
nextIndex = Math.floor(Math.random() * filteredMusik.length); nextIndex = Math.floor(Math.random() * filteredMusik.length);
@@ -146,58 +144,44 @@ const MusicPlayer = () => {
setIsPlaying(false); setIsPlaying(false);
setCurrentTime(0); setCurrentTime(0);
} }
} };
handleRepeatOrNext(audioRef, isRepeat, playNext);
}; };
const handleSeek = (value: number) => { const handleSeek = (value: number) => {
setCurrentTime(value); seekTo(audioRef, value);
if (audioRef.current) {
audioRef.current.currentTime = value;
}
}; };
const toggleMute = () => { const toggleMute = () => {
const newMuted = !isMuted; toggleMuteUtil(audioRef, isMuted, setIsMuted);
setIsMuted(newMuted);
if (audioRef.current) {
audioRef.current.muted = newMuted;
}
}; };
const handleVolumeChange = (val: number) => { const handleVolumeChange = (val: number) => {
setVolume(val); setAudioVolume(audioRef, val, setVolume, setIsMuted);
if (audioRef.current) {
audioRef.current.volume = val / 100;
}
if (val > 0 && isMuted) {
setIsMuted(false);
}
}; };
const skipBack = () => { const skipBack = () => {
if (audioRef.current) { const prevIndex = getPrevIndex(currentSongIndex, filteredMusik.length, isShuffle);
audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10); if (prevIndex >= 0) {
playSong(prevIndex);
} }
}; };
const skipForward = () => { const skipForward = () => {
if (audioRef.current) { const nextIndex = getNextIndex(currentSongIndex, filteredMusik.length, isShuffle);
audioRef.current.currentTime = Math.min(duration, audioRef.current.currentTime + 10); if (nextIndex >= 0) {
playSong(nextIndex);
} }
}; };
const togglePlayPause = () => { const toggleShuffleHandler = () => {
if (!currentSong) return; toggleShuffle(isShuffle, setIsShuffle);
};
if (isPlaying) { const togglePlayPauseHandler = () => {
audioRef.current?.pause(); if (!currentSong) return;
setIsPlaying(false); togglePlayPause(audioRef, isPlaying, setIsPlaying);
} else {
audioRef.current?.play().catch(err => {
console.error('Error playing audio:', err);
});
setIsPlaying(true);
}
}; };
if (loading) { if (loading) {
@@ -217,13 +201,16 @@ const MusicPlayer = () => {
<audio <audio
ref={audioRef} ref={audioRef}
src={currentSong.audioFile.link} src={currentSong.audioFile.link}
onEnded={handleSongEnd}
onLoadedMetadata={() => {
if (audioRef.current) {
setDuration(Math.floor(audioRef.current.duration));
}
}}
muted={isMuted} muted={isMuted}
onLoadedMetadata={() => {
if (!audioRef.current) return;
setDuration(Math.floor(audioRef.current.duration));
}}
onTimeUpdate={() => {
if (!audioRef.current || isSeeking) return;
setCurrentTime(Math.floor(audioRef.current.currentTime));
}}
onEnded={handleSongEnd}
/> />
)} )}
@@ -381,7 +368,7 @@ const MusicPlayer = () => {
<ActionIcon <ActionIcon
variant={isShuffle ? 'filled' : 'subtle'} variant={isShuffle ? 'filled' : 'subtle'}
color="#0B4F78" color="#0B4F78"
onClick={() => setIsShuffle(!isShuffle)} onClick={toggleShuffleHandler}
radius="xl" radius="xl"
> >
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />} {isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
@@ -394,7 +381,7 @@ const MusicPlayer = () => {
color="#0B4F78" color="#0B4F78"
size={56} size={56}
radius="xl" radius="xl"
onClick={togglePlayPause} onClick={togglePlayPauseHandler}
> >
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />} {isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
</ActionIcon> </ActionIcon>
@@ -415,7 +402,14 @@ const MusicPlayer = () => {
<Slider <Slider
value={currentTime} value={currentTime}
max={duration} max={duration}
onChange={handleSeek} onChange={(v) => {
setIsSeeking(true);
setCurrentTime(v); // preview
}}
onChangeEnd={(v) => {
seekTo(audioRef, v); // commit
setIsSeeking(false);
}}
color="#0B4F78" color="#0B4F78"
size="xs" size="xs"
style={{ flex: 1 }} style={{ flex: 1 }}

View File

@@ -12,6 +12,9 @@ import { Metadata, Viewport } from "next";
import { ViewTransitions } from "next-view-transitions"; import { ViewTransitions } from "next-view-transitions";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
// Force dynamic rendering untuk menghindari error prerendering
export const dynamic = 'force-dynamic';
// ✅ Pisahkan viewport ke export tersendiri // ✅ Pisahkan viewport ke export tersendiri
export const viewport: Viewport = { export const viewport: Viewport = {
width: "device-width", width: "device-width",