feat(admin): add audio recorder UI skeleton (MediaRecorder) and embed into EditorShell

This commit is contained in:
Ender 2025-10-22 00:54:02 +02:00
parent 43e6d4b53c
commit 7e0bb3dc53
2 changed files with 84 additions and 4 deletions

View File

@ -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 }) {
<Typography variant="h4" sx={{ mb: 2 }}>
Welcome to VoxBlog Editor
</Typography>
<Typography variant="body1">
Coming next: audio recorder, rich editor, AI tools, and Ghost publish flow.
</Typography>
<Box sx={{ display: 'grid', gap: 3 }}>
<Recorder />
<Typography variant="body1">
Coming next: rich editor, AI tools, and Ghost publish flow.
</Typography>
</Box>
</AdminLayout>
);
}

View File

@ -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<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const [recording, setRecording] = useState(false);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [error, setError] = useState<string>('');
const requestStream = async (): Promise<MediaStream | null> => {
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 (
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>Audio Recorder</Typography>
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<Button variant="contained" disabled={recording} onClick={startRecording}>Start</Button>
<Button variant="outlined" disabled={!recording} onClick={stopRecording}>Stop</Button>
</Stack>
{error && <Typography color="error" sx={{ mb: 2 }}>{error}</Typography>}
{audioUrl && (
<Box>
<audio controls src={audioUrl} />
</Box>
)}
</Box>
);
}