feat: implement complete React PWA client with LiveKit integration
Client React complet avec intégration LiveKit et interface PTT professionnelle : Infrastructure : - Configuration Vite avec plugin PWA (Service Worker auto-généré) - Proxy API vers serveur backend - Build optimisé et PWA manifest Composants UI : - App.jsx : écran connexion + interface principale PTT - PTTButton : bouton push-to-talk avec gestion touch/mouse events - UserList : liste participants temps réel avec indicateurs - AudioIndicator : VU-mètre avec visualisation niveau audio Fonctionnalités WebRTC : - Hook useLiveKit : connexion room, publish/subscribe, events - Gestion micro avec mute/unmute (mode PTT) - Auto-play audio participants distants - Analyseur audio pour VU-mètre - Feedback haptique (vibrations) Design : - Mode sombre par défaut - Responsive mobile-first - Animations fluides et accessibles - Support paysage mobile Phase 1.4 complétée : Client PWA opérationnel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import useLiveKit from './hooks/useLiveKit';
|
||||
import PTTButton from './components/PTTButton';
|
||||
import UserList from './components/UserList';
|
||||
import AudioIndicator from './components/AudioIndicator';
|
||||
import './App.css';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
function App() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [groupId, setGroupId] = useState('');
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
participants,
|
||||
isTalking,
|
||||
audioLevel,
|
||||
connect,
|
||||
disconnect,
|
||||
startTalking,
|
||||
stopTalking
|
||||
} = useLiveKit();
|
||||
|
||||
// Charger configuration au démarrage
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/config`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setGroups(data.groups || []);
|
||||
if (data.groups.length > 0) {
|
||||
setGroupId(data.groups[0].id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Erreur chargement config:', err);
|
||||
setError('Impossible de charger la configuration');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!username.trim()) {
|
||||
setError('Veuillez entrer votre nom');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
setError('Veuillez sélectionner un groupe');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Obtenir token du serveur
|
||||
const response = await fetch(`${API_URL}/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, groupId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur serveur');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Se connecter à LiveKit
|
||||
await connect(data.url, data.token);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Erreur connexion:', err);
|
||||
setError('Connexion impossible. Vérifiez le serveur.');
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
disconnect();
|
||||
setError(null);
|
||||
};
|
||||
|
||||
// Interface de connexion
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="login-container">
|
||||
<div className="login-card">
|
||||
<h1 className="app-title">PTT Live</h1>
|
||||
<p className="app-subtitle">Professional Intercom</p>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Nom</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Votre nom"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleConnect()}
|
||||
disabled={isConnecting}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="group">Groupe</label>
|
||||
<select
|
||||
id="group"
|
||||
value={groupId}
|
||||
onChange={(e) => setGroupId(e.target.value)}
|
||||
disabled={isConnecting || groups.length === 0}
|
||||
>
|
||||
{groups.length === 0 ? (
|
||||
<option>Chargement...</option>
|
||||
) : (
|
||||
groups.map(g => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !username.trim() || !groupId}
|
||||
>
|
||||
{isConnecting ? 'Connexion...' : 'Se connecter'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Interface principale PTT
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<div className="header-info">
|
||||
<h2>{username}</h2>
|
||||
<p className="text-secondary">
|
||||
{groups.find(g => g.id === groupId)?.name || groupId}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="btn-disconnect"
|
||||
onClick={handleDisconnect}
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
{/* Liste des participants */}
|
||||
<UserList participants={participants} />
|
||||
|
||||
{/* Indicateur audio */}
|
||||
<AudioIndicator level={audioLevel} isTalking={isTalking} />
|
||||
|
||||
{/* Bouton PTT principal */}
|
||||
<PTTButton
|
||||
isTalking={isTalking}
|
||||
onPressStart={startTalking}
|
||||
onPressEnd={stopTalking}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user