order detail + types + components

This commit is contained in:
hamid zarghami
2026-01-31 11:00:24 +03:30
parent 7df6d04d5f
commit b730f86b1e
11 changed files with 723 additions and 6 deletions
+159
View File
@@ -0,0 +1,159 @@
import { useState, useEffect, useRef, useCallback } from "react";
type UseVoiceRecorderOptions = {
onRecordingComplete?: (audioBlob: Blob) => void;
};
export const useVoiceRecorder = (options?: UseVoiceRecorderOptions) => {
const { onRecordingComplete } = options || {};
const [isRecording, setIsRecording] = useState(false);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const chunksRef = useRef<Blob[]>([]);
const animationFrameRef = useRef<number | null>(null);
const startRecording = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
chunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunksRef.current.push(event.data);
}
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(chunksRef.current, { type: "audio/webm" });
const file = new File([audioBlob], "voice-message.webm", {
type: "audio/webm",
});
const url = URL.createObjectURL(file);
setAudioUrl(url);
setAudioFile(file);
onRecordingComplete?.(audioBlob);
stream.getTracks().forEach((track) => track.stop());
};
mediaRecorder.start();
setIsRecording(true);
} catch (err) {
console.error("Microphone access error:", err);
}
}, [onRecordingComplete]);
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop();
setIsRecording(false);
}
}, [isRecording]);
const resetRecorder = useCallback(() => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop();
}
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
chunksRef.current = [];
setIsRecording(false);
setIsPlaying(false);
setCurrentTime(0);
setDuration(0);
setAudioUrl(null);
setAudioFile(null);
}, [audioUrl, isRecording]);
const togglePlayPause = useCallback(() => {
if (!audioRef.current) return;
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying((prev) => !prev);
}, [isPlaying]);
const updateProgress = useCallback(() => {
if (!audioRef.current) return;
setCurrentTime(audioRef.current.currentTime);
if (isPlaying) {
animationFrameRef.current = requestAnimationFrame(updateProgress);
}
}, [isPlaying]);
useEffect(() => {
if (!audioRef.current) return;
const audio = audioRef.current;
const onLoaded = () => setDuration(audio.duration);
const onEnded = () => {
setIsPlaying(false);
setCurrentTime(0);
};
audio.addEventListener("loadedmetadata", onLoaded);
audio.addEventListener("ended", onEnded);
return () => {
audio.removeEventListener("loadedmetadata", onLoaded);
audio.removeEventListener("ended", onEnded);
};
}, [audioUrl]);
useEffect(() => {
if (isPlaying) updateProgress();
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isPlaying, updateProgress]);
return {
// state
isRecording,
isPlaying,
audioUrl,
audioFile,
currentTime,
duration,
// refs
audioRef,
// actions
startRecording,
stopRecording,
togglePlayPause,
resetRecorder,
// helpers
progress: duration ? currentTime / duration : 0,
};
};