diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index b6706e0..b119594 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -1,5 +1,6 @@ -import { Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import AdminLayout from '../layout/AdminLayout'; +import Recorder from '../features/recorder/Recorder'; export default function EditorShell({ onLogout }: { onLogout?: () => void }) { return ( @@ -7,9 +8,12 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) { Welcome to VoxBlog Editor - - Coming next: audio recorder, rich editor, AI tools, and Ghost publish flow. - + + + + Coming next: rich editor, AI tools, and Ghost publish flow. + + ); } diff --git a/apps/admin/src/features/recorder/Recorder.tsx b/apps/admin/src/features/recorder/Recorder.tsx new file mode 100644 index 0000000..165a31f --- /dev/null +++ b/apps/admin/src/features/recorder/Recorder.tsx @@ -0,0 +1,76 @@ +import { useEffect, useRef, useState } from 'react'; +import { Box, Button, Stack, Typography } from '@mui/material'; + +export default function Recorder() { + const mediaRecorderRef = useRef(null); + const chunksRef = useRef([]); + const [recording, setRecording] = useState(false); + const [audioUrl, setAudioUrl] = useState(null); + const [error, setError] = useState(''); + + const requestStream = async (): Promise => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + return stream; + } catch (e) { + setError('Microphone permission denied'); + return null; + } + }; + + const startRecording = async () => { + setError(''); + const stream = await requestStream(); + if (!stream) return; + + const mr = new MediaRecorder(stream); + mediaRecorderRef.current = mr; + chunksRef.current = []; + + mr.ondataavailable = (e: BlobEvent) => { + if (e.data && e.data.size > 0) { + chunksRef.current.push(e.data); + } + }; + mr.onstop = () => { + const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); + const url = URL.createObjectURL(blob); + setAudioUrl((prev) => { + if (prev) URL.revokeObjectURL(prev); + return url; + }); + // stop all tracks to release mic + stream.getTracks().forEach(t => t.stop()); + }; + + mr.start(); + setRecording(true); + }; + + const stopRecording = () => { + mediaRecorderRef.current?.stop(); + setRecording(false); + }; + + useEffect(() => { + return () => { + if (audioUrl) URL.revokeObjectURL(audioUrl); + }; + }, [audioUrl]); + + return ( + + Audio Recorder + + + + + {error && {error}} + {audioUrl && ( + + + )} + + ); +}