Une fois connecté, plus aucun lien vitrine : nouvelle layouts/app (barre
du haut avec menu avatar, barre latérale par rôle, bulle de chat
Messenger, barre d'onglets mobile). Les 10 entrées de navigation
demandées sont construites en vrai plutôt qu'en renvoi "bientôt
disponible" : Ma classe, Emploi du temps, Mes devoirs, Mes cours, Mes
notes côté élève ; Mes classes, Emploi du temps, Séances en direct,
Bibliothèque, Résultats (avec Observations, la dernière fonction
fictive du tableau de bord démo) côté prof.
Chat élève <-> prof de sa classe, jamais entre élèves. Mon profil,
Paramètres (thème, taille du texte), Signaler un problème, Mes
raccourcis. /eleve vidée des faux contenus ("À faire", "Ma progression",
"Mes QCM") restés avant le chantier "vrai cockpit".
Bug trouvé en testant en réel sur le serveur (pas en local) : le
compteur de messages non lus du chat ne redescendait jamais à zéro
(FilConversation sans lu_par_prof_a/lu_par_eleve_a dans son fillable) —
corrigé avec un test de non-régression.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
7.5 KiB
PHP
100 lines
7.5 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('titre', 'Visio · '.$seance->classe->nom)
|
||
|
||
{{-- Étape 15 : reprend design/resources/views/visio.blade.php (vue élève) ;
|
||
l'image factice est remplacée par la vraie visio (Jitsi, IFrame API). --}}
|
||
@section('contenu')
|
||
<main style="background:#f8f9fa">
|
||
<section style="padding:20px 20px 72px"><div style="max-width:1100px;margin:0 auto;background:#fff;border:1px solid #e3e5e8;border-radius:20px;overflow:hidden">
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid #e3e5e8"><span style="font-size:14px;font-weight:700;color:#0b1220">{{ $seance->classe->nom }} · {{ $seance->lab->titre }}</span><span data-statut-e style="display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 14px;border-radius:999px;font-size:14px;font-weight:700;background:#eef0f3;color:#3c4043">{{ $jeton ? 'En sourdine' : 'En attente' }}</span></div>
|
||
@if ($jeton)
|
||
<div style="padding:20px"><div id="v-scene" style="position:relative;aspect-ratio:16/9;border-radius:16px;overflow:hidden;background:#202124"></div></div>
|
||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:12px;padding:4px 20px 24px">
|
||
<button type="button" data-micro-e aria-label="Micro" style="width:52px;height:52px;border-radius:50%;display:grid;place-items:center;border:1px solid #dadce0;background:#fff;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v3"/></svg></button>
|
||
<button type="button" data-cam-e aria-label="Caméra" style="width:52px;height:52px;border-radius:50%;display:grid;place-items:center;border:1px solid #dadce0;background:#fff;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg></button>
|
||
<button type="button" data-main-e aria-pressed="false" class="border border-[#e3e5e8] rounded-2xl bg-[#fce8e6] px-5 py-4 text-left text-lg font-extrabold aria-pressed:bg-[#fef7e0]" style="display:inline-flex;align-items:center;gap:10px;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 11V6a2 2 0 0 0-4 0v5"/><path d="M14 10V4a2 2 0 0 0-4 0v6"/><path d="M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg><span>Lever la main</span></button>
|
||
<a href="{{ route('seances.eleve.attente', $seance) }}" data-quitter style="display:inline-flex;align-items:center;gap:8px;font-size:14px;font-weight:700;cursor:pointer;border-radius:999px;height:52px;padding:0 22px;background:#d93025;color:#fff;text-decoration:none"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>Quitter</a>
|
||
</div>
|
||
<p data-aide-e style="margin:0;padding:0 20px 24px;text-align:center;font-size:14px;font-weight:600;color:#5f6368">Tu es en sourdine. Lève la main pour demander la parole.</p>
|
||
@else
|
||
<p style="margin:0;padding:48px 20px;text-align:center;font-size:18px;font-weight:700;color:#3c4043">Ton prof n’a pas encore ouvert la visio. Garde cette page ouverte : tu entreras dès qu’il l’ouvrira.</p>
|
||
@endif
|
||
</div></section>
|
||
</main>
|
||
|
||
@if ($jeton)
|
||
<script nonce="{{ Vite::cspNonce() }}" src="https://{{ config('visio.domaine') }}/external_api.js"></script>
|
||
@endif
|
||
<script nonce="{{ Vite::cspNonce() }}">
|
||
(function () {
|
||
var URL_ETAT = @json(route('seances.visio.eleve.etat', $seance));
|
||
var URL_MAIN = @json(route('seances.visio.eleve.main', $seance));
|
||
var URL_PARTICIPANT = @json(route('seances.visio.eleve.participant', $seance));
|
||
var CSRF = @json(csrf_token());
|
||
var OUVERTE = @json((bool) $jeton);
|
||
var $ = function (s) { return document.querySelector(s); };
|
||
var poster = function (url, donnees) {
|
||
return fetch(url, { method: 'POST', headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(donnees || {}) });
|
||
};
|
||
var api = null, moi = null, mainLevee = false, chat = null;
|
||
var BOUTONS = ['microphone', 'camera', 'raisehand', 'tileview', 'fullscreen', 'hangup'];
|
||
|
||
function statut(texte, couleur, fond, aide) {
|
||
var s = $('[data-statut-e]'); s.textContent = texte; s.style.color = couleur; s.style.background = fond;
|
||
var a = $('[data-aide-e]'); if (a && aide) a.textContent = aide;
|
||
}
|
||
|
||
@if ($jeton)
|
||
api = new JitsiMeetExternalAPI(@json(config('visio.domaine')), {
|
||
roomName: @json($seance->visio_salle),
|
||
jwt: @json($jeton),
|
||
parentNode: $('#v-scene'),
|
||
width: '100%', height: '100%',
|
||
configOverwrite: {
|
||
startWithAudioMuted: true, startWithVideoMuted: true, readOnlyName: true,
|
||
prejoinConfig: { enabled: false }, disableDeepLinking: true,
|
||
toolbarButtons: BOUTONS.concat(@json($seance->visio_chat) ? ['chat'] : [])
|
||
}
|
||
});
|
||
api.addListener('videoConferenceJoined', function (e) {
|
||
moi = e.id;
|
||
poster(URL_PARTICIPANT, { participant_id: e.id });
|
||
});
|
||
// La main levée passe toujours par Jitsi (bouton de la page ou de Jitsi) ;
|
||
// l'application suit l'événement : une seule source, pas de désaccord.
|
||
api.addListener('raiseHandUpdated', function (e) {
|
||
if (e.id !== moi) return;
|
||
mainLevee = !!e.handRaised;
|
||
$('[data-main-e]').setAttribute('aria-pressed', String(mainLevee));
|
||
$('[data-main-e] span').textContent = mainLevee ? 'Baisser la main' : 'Lever la main';
|
||
poster(URL_MAIN, { levee: mainLevee });
|
||
});
|
||
api.addListener('audioMuteStatusChanged', function (e) {
|
||
if (e.muted) statut('En sourdine', '#3c4043', '#eef0f3', 'Tu es en sourdine. Lève la main pour demander la parole.');
|
||
else statut('Tu as la parole', '#0d652d', '#e6f4ea', 'Ton prof t’a donné la parole. Coupe ton micro quand tu as fini.');
|
||
});
|
||
api.addListener('readyToClose', function () { location.href = $('[data-quitter]').href; });
|
||
$('[data-main-e]').addEventListener('click', function () { api.executeCommand('toggleRaiseHand'); });
|
||
$('[data-micro-e]').addEventListener('click', function () { api.executeCommand('toggleAudio'); });
|
||
$('[data-cam-e]').addEventListener('click', function () { api.executeCommand('toggleVideo'); });
|
||
$('[data-quitter]').addEventListener('click', function () { try { api.executeCommand('hangup'); } catch (e) {} });
|
||
@endif
|
||
|
||
function rafraichir() {
|
||
fetch(URL_ETAT, { headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json(); }).then(function (e) {
|
||
if (e.ouverte !== OUVERTE) { location.reload(); return; }
|
||
if (!api) return;
|
||
// Le prof a donné la parole : l'application a baissé la main, Jitsi suit.
|
||
if (mainLevee && !e.main_levee) api.executeCommand('toggleRaiseHand');
|
||
if (chat !== e.chat) {
|
||
chat = e.chat;
|
||
api.executeCommand('overwriteConfig', { toolbarButtons: BOUTONS.concat(chat ? ['chat'] : []) });
|
||
}
|
||
}).catch(function () {});
|
||
}
|
||
rafraichir();
|
||
setInterval(rafraichir, 2000);
|
||
})();
|
||
</script>
|
||
@endsection
|