bibihezibra's picture
Build a web app for recording audio translations with these features:
7050e07 verified
// Initialize Supabase client
const supabaseUrl = 'https://your-project.supabase.co';
const supabaseKey = 'your-anon-key';
const supabase = window.supabase.createClient(supabaseUrl, supabaseKey);
// App State
let currentView = 'dashboard';
let recordingSession = {
sentences: [],
currentIndex: 0,
recordings: [],
startTime: null
};
let mediaRecorder = null;
let audioChunks = [];
let recordingStartTime = null;
// DOM Elements
const views = {
dashboard: document.getElementById('dashboard-view'),
recording: document.getElementById('recording-view'),
admin: document.getElementById('admin-view')
};
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
initializeEventListeners();
loadDashboardData();
checkAdminAccess();
});
// Event Listeners
function initializeEventListeners() {
// Dashboard
document.getElementById('start-recording-btn').addEventListener('click', startRecordingSession);
document.getElementById('recent-recordings-btn').addEventListener('click', toggleRecentWork);
// Recording interface
document.getElementById('record-btn').addEventListener('click', toggleRecording);
document.getElementById('skip-btn').addEventListener('click', skipSentence);
document.getElementById('next-btn').addEventListener('click', nextSentence);
// Navigation will be handled by navbar component
}
// Navigation
function showView(viewName) {
Object.keys(views).forEach(key => {
views[key].classList.add('hidden');
});
if (views[viewName]) {
views[viewName].classList.remove('hidden');
currentView = viewName;
if (viewName === 'admin') {
loadAdminData();
} else if (viewName === 'dashboard') {
loadDashboardData();
}
}
}
// Dashboard Functions
async function loadDashboardData() {
try {
// Mock data for MVP
const mockData = {
totalEarnings: '12.50',
completedCount: 125,
pendingCount: 8
};
document.getElementById('total-earnings').textContent = mockData.totalEarnings;
document.getElementById('completed-count').textContent = mockData.completedCount;
document.getElementById('pending-count').textContent = mockData.pendingCount;
loadRecentRecordings();
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function loadRecentRecordings() {
// Mock recent recordings
const recentRecordings = [
{ id: 1, frenchText: "Bonjour, comment allez-vous?", status: 'approved', date: '2024-01-15' },
{ id: 2, frenchText: "Je voudrais un café", status: 'pending', date: '2024-01-15' },
{ id: 3, frenchText: "Où est la gare?", status: 'approved', date: '2024-01-14' }
];
const recentList = document.getElementById('recent-list');
recentList.innerHTML = recentRecordings.map(rec => `
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex-1">
<p class="text-sm text-gray-800">${rec.frenchText}</p>
<p class="text-xs text-gray-500">${rec.date}</p>
</div>
<span class="px-2 py-1 text-xs font-medium rounded-full ${
rec.status === 'approved' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}">
${rec.status}
</span>
</div>
`).join('');
}
function toggleRecentWork() {
const recentWork = document.getElementById('recent-work');
recentWork.classList.toggle('hidden');
}
// Recording Session Functions
async function startRecordingSession() {
try {
// Load 10 random sentences (mock data for MVP)
recordingSession.sentences = [
{ id: 1, french_text: "Bonjour, comment allez-vous?", domain: "greetings" },
{ id: 2, french_text: "Je voudrais un café, s'il vous plaît", domain: "food" },
{ id: 3, french_text: "Où se trouve la bibliothèque?", domain: "directions" },
{ id: 4, french_text: "Quel temps fait-il aujourd'hui?", domain: "weather" },
{ id: 5, french_text: "Merci beaucoup pour votre aide", domain: "gratitude" },
{ id: 6, french_text: "Je ne comprends pas", domain: "communication" },
{ id: 7, french_text: "À quelle heure fermez-vous?", domain: "time" },
{ id: 8, french_text: "Combien ça coûte?", domain: "shopping" },
{ id: 9, french_text: "Pouvez-vous répéter, s'il vous plaît?", domain: "communication" },
{ id: 10, french_text: "J'ai besoin d'un taxi", domain: "transport" }
];
recordingSession.currentIndex = 0;
recordingSession.recordings = [];
recordingSession.startTime = new Date();
showView('recording');
loadCurrentSentence();
} catch (error) {
console.error('Error starting session:', error);
alert('Error starting recording session');
}
}
function loadCurrentSentence() {
const sentence = recordingSession.sentences[recordingSession.currentIndex];
if (!sentence) return;
document.getElementById('current-sentence').textContent = recordingSession.currentIndex + 1;
document.getElementById('progress-bar').style.width = `${((recordingSession.currentIndex + 1) / 10) * 100}%`;
document.getElementById('french-sentence').textContent = sentence.french_text;
// Reset UI
resetRecordingUI();
// Load French audio (mock - in real app would fetch from storage)
const frenchAudio = document.getElementById('french-audio');
frenchAudio.src = `https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3?t=${sentence.id}`;
}
function resetRecordingUI() {
const recordBtn = document.getElementById('record-btn');
const recordingIndicator = document.getElementById('recording-indicator');
const pulaarAudioSection = document.getElementById('pulaar-audio-section');
const nextBtn = document.getElementById('next-btn');
recordBtn.innerHTML = '<i data-feather="mic" class="w-12 h-12"></i>';
recordBtn.classList.remove('recording-active', 'bg-green-500', 'hover:bg-green-600');
recordBtn.classList.add('bg-red-500', 'hover:bg-red-600');
recordingIndicator.classList.add('hidden');
pulaarAudioSection.classList.add('hidden');
nextBtn.disabled = true;
feather.replace();
}
// Recording Functions
async function toggleRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
// Show the recording
const pulaarAudio = document.getElementById('pulaar-audio');
pulaarAudio.src = audioUrl;
document.getElementById('pulaar-audio-section').classList.remove('hidden');
// Enable next button
document.getElementById('next-btn').disabled = false;
// Store recording
recordingSession.recordings[recordingSession.currentIndex] = {
audioBlob: audioBlob,
audioUrl: audioUrl,
timestamp: new Date()
};
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
recordingStartTime = Date.now();
// Update UI
const recordBtn = document.getElementById('record-btn');
recordBtn.innerHTML = '<i data-feather="square" class="w-12 h-12"></i>';
recordBtn.classList.remove('bg-red-500', 'hover:bg-red-600');
recordBtn.classList.add('bg-green-500', 'hover:bg-green-600', 'recording-active');
document.getElementById('recording-indicator').classList.remove('hidden');
// Update timer
updateRecordingTimer();
feather.replace();
} catch (error) {
console.error('Error starting recording:', error);
alert('Could not access microphone. Please check permissions.');
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
}
function updateRecordingTimer() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
const seconds = (elapsed % 60).toString().padStart(2, '0');
document.getElementById('recording-timer').textContent = `${minutes}:${seconds}`;
requestAnimationFrame(updateRecordingTimer);
}
}
function skipSentence() {
recordingSession.recordings[recordingSession.currentIndex] = null;
nextSentence();
}
function nextSentence() {
recordingSession.currentIndex++;
if (recordingSession.currentIndex >= recordingSession.sentences.length) {
// Session complete
finishRecordingSession();
} else {
loadCurrentSentence();
}
}
async function finishRecordingSession() {
// Calculate earnings
const completedRecordings = recordingSession.recordings.filter(r => r !== null).length;
const earnings = completedRecordings * 0.10;
// Show completion message
alert(`Session Complete!\n\nCompleted recordings: ${completedRecordings}\nEstimated earnings: €${earnings.toFixed(2)}\n\nYour recordings are now pending review.`);
// Return to dashboard
showView('dashboard');
// Refresh dashboard data
loadDashboardData();
}
// Admin Functions
function checkAdminAccess() {
// Check if user is admin (mock for MVP)
const isAdmin = window.location.hash === '#admin';
if (isAdmin) {
// In real app, check auth role
}
}
async function loadAdminData() {
try {
// Mock data for MVP
const mockSubmissions = [
{
id: 1,
userName: 'User 1',
frenchText: "Bonjour, comment allez-vous?",
frenchAudioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
pulaarAudioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
status: 'pending',
createdAt: '2024-01-15T10:30:00Z'
},
{
id: 2,
userName: 'User 2',
frenchText: "Je voudrais un café",
frenchAudioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
pulaarAudioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
status: 'pending',
createdAt: '2024-01-15T09:45:00Z'
}
];
// Update stats
document.getElementById('admin-total-submissions').textContent = mockSubmissions.length;
document.getElementById('admin-pending').textContent = mockSubmissions.filter(s => s.status === 'pending').length;
document.getElementById('admin-approved').textContent = 0;
document.getElementById('admin-rejected').textContent = 0;
// Render submissions
renderSubmissions(mockSubmissions);
} catch (error) {
console.error('Error loading admin data:', error);
}
}
function renderSubmissions(submissions) {
const submissionsList = document.getElementById('submissions-list');
submissionsList.innerHTML = submissions.map(sub => `
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div class="mb-3">
<div class="flex justify-between items-start mb-2">
<div>
<p class="font-medium text-gray-800">${sub.userName}</p>
<p class="text-sm text-gray-600">${new Date(sub.createdAt).toLocaleString()}</p>
</div>
<span class="px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800">
${sub.status}
</span>
</div>
<p class="text-gray-700">${sub.frenchText}</p>
</div>
<div class="grid md:grid-cols-2 gap-4 mb-4">
<div>
<p class="text-sm font-medium text-gray-600 mb-1">French Audio:</p>
<audio controls class="w-full">
<source src="${sub.frenchAudioUrl}" type="audio/mpeg">
</audio>
</div>
<div>
<p class="text-sm font-medium text-gray-600 mb-1">Pulaar Audio:</p>
<audio controls class="w-full">
<source src="${sub.pulaarAudioUrl}" type="audio/mpeg">
</audio>
</div>
</div>
<div class="flex gap-3">
<button onclick="updateSubmissionStatus(${sub.id}, 'approved')" class="flex-1 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
<i data-feather="check" class="w-4 h-4 inline mr-1"></i>
Approve
</button>
<button onclick="updateSubmissionStatus(${sub.id}, 'rejected')" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
<i data-feather="x" class="w-4 h-4 inline mr-1"></i>
Reject
</button>
</div>
</div>
`).join('');
feather.replace();
}
async function updateSubmissionStatus(submissionId, status) {
try {
// In real app, update in Supabase
console.log(`Updating submission ${submissionId} to ${status}`);
// Reload data
loadAdminData();
// Show feedback
alert(`Submission ${status} successfully!`);
} catch (error) {
console.error('Error updating submission:', error);
alert('Error updating submission status');
}
}
// Make functions globally available for onclick handlers
window.updateSubmissionStatus = updateSubmissionStatus;
window.showView = showView;