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:
2026-05-21 14:48:18 +02:00
parent 5e74f0dcdf
commit 0640a9f0b6
15 changed files with 1568 additions and 32 deletions
+114
View File
@@ -0,0 +1,114 @@
import { useEffect, useRef } from 'react';
import './PTTButton.css';
/**
* Bouton PTT principal
* Gère touch et mouse events pour desktop et mobile
*/
export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
const buttonRef = useRef(null);
const isPressingRef = useRef(false);
useEffect(() => {
const button = buttonRef.current;
if (!button) return;
// Empêcher comportements par défaut
const preventDefault = (e) => {
e.preventDefault();
};
// Touch events (mobile)
const handleTouchStart = (e) => {
e.preventDefault();
if (!isPressingRef.current) {
isPressingRef.current = true;
onPressStart();
}
};
const handleTouchEnd = (e) => {
e.preventDefault();
if (isPressingRef.current) {
isPressingRef.current = false;
onPressEnd();
}
};
// Mouse events (desktop)
const handleMouseDown = (e) => {
e.preventDefault();
if (!isPressingRef.current) {
isPressingRef.current = true;
onPressStart();
}
};
const handleMouseUp = (e) => {
e.preventDefault();
if (isPressingRef.current) {
isPressingRef.current = false;
onPressEnd();
}
};
const handleMouseLeave = (e) => {
// Si on quitte le bouton en maintenant, on arrête
if (isPressingRef.current) {
isPressingRef.current = false;
onPressEnd();
}
};
// Attacher events
button.addEventListener('touchstart', handleTouchStart, { passive: false });
button.addEventListener('touchend', handleTouchEnd, { passive: false });
button.addEventListener('touchcancel', handleTouchEnd, { passive: false });
button.addEventListener('mousedown', handleMouseDown);
button.addEventListener('mouseup', handleMouseUp);
button.addEventListener('mouseleave', handleMouseLeave);
button.addEventListener('contextmenu', preventDefault);
return () => {
button.removeEventListener('touchstart', handleTouchStart);
button.removeEventListener('touchend', handleTouchEnd);
button.removeEventListener('touchcancel', handleTouchEnd);
button.removeEventListener('mousedown', handleMouseDown);
button.removeEventListener('mouseup', handleMouseUp);
button.removeEventListener('mouseleave', handleMouseLeave);
button.removeEventListener('contextmenu', preventDefault);
};
}, [onPressStart, onPressEnd]);
return (
<div className="ptt-container">
<button
ref={buttonRef}
className={`ptt-button ${isTalking ? 'talking' : ''}`}
type="button"
>
<div className="ptt-icon">
{isTalking ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
<path d="M19 11h2v2h-2zm-16 0h2v2H3z"/>
</svg>
)}
</div>
<span className="ptt-label">
{isTalking ? 'En cours...' : 'Maintenir pour parler'}
</span>
</button>
<p className="ptt-hint">
{isTalking ? 'Relâchez pour arrêter' : 'Appuyez et maintenez le bouton'}
</p>
</div>
);
}