299 lines
16 KiB
PHP
299 lines
16 KiB
PHP
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="fr" data-theme="{{ $compte->preferenceTheme() }}" data-taille="{{ $compte->preferenceTailleTexte() }}">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|||
|
|
<title>@yield('titre') · Syllabus</title>
|
|||
|
|
{{ Vite::fonts() }}
|
|||
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|||
|
|
<link rel="stylesheet" href="{{ asset('css/syllabus-theme.css') }}">
|
|||
|
|
<link rel="stylesheet" href="{{ asset('css/app-shell.css') }}">
|
|||
|
|
@stack('head')
|
|||
|
|
</head>
|
|||
|
|
<body class="app-corps font-display antialiased">
|
|||
|
|
|
|||
|
|
<header class="app-topbar">
|
|||
|
|
<a href="{{ $estProf ? route('mes-classes.index') : route('eleve') }}" class="app-topbar__logo" aria-label="Syllabus, accueil">
|
|||
|
|
<img src="{{ asset('images/logo-syllabus-icone.png') }}" alt="">
|
|||
|
|
<span>Syllabus</span>
|
|||
|
|
</a>
|
|||
|
|
<div class="app-topbar__avatar">
|
|||
|
|
<button type="button" id="app-avatar-bouton" aria-haspopup="true" aria-expanded="false" aria-controls="app-avatar-menu">
|
|||
|
|
<span class="app-topbar__avatar-cercle" aria-hidden="true">{{ mb_substr($nomAffiche, 0, 1) }}</span>
|
|||
|
|
<span class="hidden sm:inline">{{ $nomAffiche }}</span>
|
|||
|
|
</button>
|
|||
|
|
<div id="app-avatar-menu" class="app-topbar__menu" hidden role="menu">
|
|||
|
|
<a href="{{ route('profil') }}" role="menuitem">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.5-6 8-6s8 2 8 6"/></svg>
|
|||
|
|
Mon profil
|
|||
|
|
</a>
|
|||
|
|
<a href="{{ route('parametres') }}" role="menuitem">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 13a7.6 7.6 0 0 0 0-2l2-1.5-2-3.4-2.3.9a7.6 7.6 0 0 0-1.7-1l-.3-2.5h-4l-.3 2.5a7.6 7.6 0 0 0-1.7 1l-2.3-.9-2 3.4L6.6 11a7.6 7.6 0 0 0 0 2l-2 1.5 2 3.4 2.3-.9c.5.4 1.1.8 1.7 1l.3 2.5h4l.3-2.5c.6-.2 1.2-.6 1.7-1l2.3.9 2-3.4-2-1.5Z"/></svg>
|
|||
|
|
Paramètres
|
|||
|
|
</a>
|
|||
|
|
<a href="{{ route('aide.app') }}" role="menuitem">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.8.4-1 .8-1 1.7v.5"/><path d="M12 17h.01"/></svg>
|
|||
|
|
Aide et assistance
|
|||
|
|
</a>
|
|||
|
|
<button type="button" id="app-signaler-bouton" role="menuitem">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 3v18"/><path d="M5 4h11l-2.5 3.5L16 11H5"/></svg>
|
|||
|
|
Signaler un problème
|
|||
|
|
</button>
|
|||
|
|
<hr>
|
|||
|
|
<form action="{{ route('deconnexion') }}" method="post">@csrf
|
|||
|
|
<button type="submit" role="menuitem">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg>
|
|||
|
|
Se déconnecter
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="app-shell">
|
|||
|
|
<nav class="app-sidebar" aria-label="Navigation de l'application">
|
|||
|
|
<ul class="app-sidebar__nav">
|
|||
|
|
@include('partials.nav-application')
|
|||
|
|
</ul>
|
|||
|
|
<p class="app-sidebar__titre">Mes raccourcis</p>
|
|||
|
|
<ul class="app-sidebar__nav">
|
|||
|
|
@foreach ($raccourcis as $raccourci)
|
|||
|
|
<li class="app-sidebar__raccourci">
|
|||
|
|
<a href="{{ $raccourci->url }}">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 17v5"/><path d="M8 3h8l-1 6 2 2v2H7v-2l2-2-1-6Z"/></svg>
|
|||
|
|
{{ $raccourci->label }}
|
|||
|
|
</a>
|
|||
|
|
<form action="{{ route('raccourcis.destroy', $raccourci) }}" method="post" onsubmit="return confirm('Retirer ce raccourci ?')">
|
|||
|
|
@csrf
|
|||
|
|
<button type="submit" aria-label="Retirer ce raccourci">×</button>
|
|||
|
|
</form>
|
|||
|
|
</li>
|
|||
|
|
@endforeach
|
|||
|
|
</ul>
|
|||
|
|
<button type="button" id="app-epingler-bouton" class="app-sidebar__epingler">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M12 5v14M5 12h14"/></svg>
|
|||
|
|
Épingler cette page
|
|||
|
|
</button>
|
|||
|
|
<form action="{{ route('raccourcis.store') }}" method="post" id="app-epingler-form" hidden class="mt-2 grid gap-2 px-1">
|
|||
|
|
@csrf
|
|||
|
|
<input type="hidden" name="url" id="app-epingler-url">
|
|||
|
|
<input type="text" name="label" id="app-epingler-label" required maxlength="80" class="rounded-lg border px-2 py-1 text-sm" style="border-color:var(--sy-line);background:var(--sy-bg);color:var(--sy-ink)">
|
|||
|
|
<button type="submit" class="rounded-lg px-2 py-1 text-sm font-bold text-white" style="background:var(--sy-blue)">Épingler</button>
|
|||
|
|
</form>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<main class="app-main">
|
|||
|
|
@yield('contenu')
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<nav class="app-tabbar" aria-label="Navigation (mobile)">
|
|||
|
|
<ul class="app-tabbar__liste">
|
|||
|
|
@include('partials.nav-application', ['mobile' => true])
|
|||
|
|
<li>
|
|||
|
|
<button type="button" id="app-plus-bouton">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>
|
|||
|
|
Plus
|
|||
|
|
</button>
|
|||
|
|
</li>
|
|||
|
|
</ul>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<div class="app-feuille" id="app-feuille-plus" role="dialog" aria-label="Plus">
|
|||
|
|
<div class="app-feuille__entete">
|
|||
|
|
<strong>Plus</strong>
|
|||
|
|
<button type="button" id="app-feuille-fermer" aria-label="Fermer">✕</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="app-feuille__contenu">
|
|||
|
|
@include('partials.nav-application', ['mobilePlus' => true])
|
|||
|
|
<hr style="border-color:var(--sy-line);margin:8px 0">
|
|||
|
|
<a href="{{ route('profil') }}">Mon profil</a>
|
|||
|
|
<a href="{{ route('parametres') }}">Paramètres</a>
|
|||
|
|
<a href="{{ route('aide.app') }}">Aide et assistance</a>
|
|||
|
|
<form action="{{ route('deconnexion') }}" method="post">@csrf<button type="submit">Se déconnecter</button></form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="app-chat-bulle" role="presentation">
|
|||
|
|
<button type="button" id="app-chat-bouton" aria-label="Ouvrir le chat" aria-expanded="false">
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
|
|||
|
|
<span class="app-chat-bulle__badge" id="app-chat-badge" hidden></span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="app-chat-fenetre" id="app-chat-fenetre" hidden></div>
|
|||
|
|
|
|||
|
|
<div id="app-signaler-fenetre" hidden style="position:fixed;inset:0;z-index:80;background:rgba(15,23,42,.4);display:flex;align-items:center;justify-content:center;padding:20px">
|
|||
|
|
<form action="{{ route('signalements.store') }}" method="post" style="background:var(--sy-surface);border-radius:16px;padding:20px;max-width:420px;width:100%;display:grid;gap:10px">
|
|||
|
|
@csrf
|
|||
|
|
<input type="hidden" name="page_url" value="{{ request()->fullUrl() }}">
|
|||
|
|
<p style="font-weight:800;color:var(--sy-ink)">Signaler un problème</p>
|
|||
|
|
<textarea name="message" required maxlength="2000" rows="4" placeholder="Décris ce qui ne marche pas…" style="border:1px solid var(--sy-line);border-radius:10px;padding:10px;background:var(--sy-bg);color:var(--sy-ink)"></textarea>
|
|||
|
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|||
|
|
<button type="button" id="app-signaler-annuler" style="border:0;background:none;font-weight:700;color:var(--sy-muted)">Annuler</button>
|
|||
|
|
<button type="submit" style="border:0;border-radius:999px;background:var(--sy-blue);color:#fff;padding:8px 18px;font-weight:800">Envoyer</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script nonce="{{ Vite::cspNonce() }}">
|
|||
|
|
(function () {
|
|||
|
|
function basculer(bouton, panneau) {
|
|||
|
|
var ouvert = !panneau.hidden;
|
|||
|
|
panneau.hidden = ouvert;
|
|||
|
|
bouton.setAttribute('aria-expanded', String(!ouvert));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var avatarBtn = document.getElementById('app-avatar-bouton');
|
|||
|
|
var avatarMenu = document.getElementById('app-avatar-menu');
|
|||
|
|
avatarBtn.addEventListener('click', function (e) { e.stopPropagation(); basculer(avatarBtn, avatarMenu); });
|
|||
|
|
document.addEventListener('click', function (e) { if (!avatarMenu.hidden && !avatarMenu.contains(e.target) && e.target !== avatarBtn) avatarMenu.hidden = true; });
|
|||
|
|
|
|||
|
|
var plusBtn = document.getElementById('app-plus-bouton');
|
|||
|
|
var feuille = document.getElementById('app-feuille-plus');
|
|||
|
|
if (plusBtn) plusBtn.addEventListener('click', function () { feuille.style.display = 'block'; });
|
|||
|
|
document.getElementById('app-feuille-fermer').addEventListener('click', function () { feuille.style.display = 'none'; });
|
|||
|
|
|
|||
|
|
var signalerBtn = document.getElementById('app-signaler-bouton');
|
|||
|
|
var signalerFenetre = document.getElementById('app-signaler-fenetre');
|
|||
|
|
signalerBtn.addEventListener('click', function () { avatarMenu.hidden = true; signalerFenetre.style.display = 'flex'; });
|
|||
|
|
document.getElementById('app-signaler-annuler').addEventListener('click', function () { signalerFenetre.style.display = 'none'; });
|
|||
|
|
|
|||
|
|
var epinglerBtn = document.getElementById('app-epingler-bouton');
|
|||
|
|
var epinglerForm = document.getElementById('app-epingler-form');
|
|||
|
|
if (epinglerBtn) epinglerBtn.addEventListener('click', function () {
|
|||
|
|
document.getElementById('app-epingler-url').value = window.location.pathname;
|
|||
|
|
document.getElementById('app-epingler-label').value = document.title.replace(/ · Syllabus$/, '');
|
|||
|
|
epinglerForm.hidden = false;
|
|||
|
|
epinglerBtn.hidden = true;
|
|||
|
|
});
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
<script nonce="{{ Vite::cspNonce() }}">
|
|||
|
|
(function () {
|
|||
|
|
var ROLE = @json($estProf ? 'prof' : 'eleve');
|
|||
|
|
var CSRF = @json(csrf_token());
|
|||
|
|
var URLS = @if ($estProf)
|
|||
|
|
{ index: @json(route('chat.prof.index')), etat: @json(url('/chat')) + '/__ID__/etat', envoyer: @json(url('/chat')) + '/__ID__/envoyer' }
|
|||
|
|
@else
|
|||
|
|
{ etat: @json(route('chat.eleve.etat')), envoyer: @json(route('chat.eleve.envoyer')) }
|
|||
|
|
@endif;
|
|||
|
|
|
|||
|
|
var bulle = document.getElementById('app-chat-bouton');
|
|||
|
|
var fenetre = document.getElementById('app-chat-fenetre');
|
|||
|
|
var badge = document.getElementById('app-chat-badge');
|
|||
|
|
var ouverte = false, filActuel = null, minuteur = null;
|
|||
|
|
|
|||
|
|
function echapper(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|||
|
|
|
|||
|
|
function rendreMessages(corps, messages, actif) {
|
|||
|
|
var html = '<div class="app-chat-fenetre__corps" id="app-chat-corps">';
|
|||
|
|
if (messages.length === 0) html += '<p style="color:var(--sy-muted);font-size:13px;font-weight:600">Aucun message pour l’instant.</p>';
|
|||
|
|
messages.forEach(function (m) {
|
|||
|
|
html += '<p class="app-bulle-message app-bulle-message--' + m.expediteur + '">' + echapper(m.contenu) + '</p>';
|
|||
|
|
});
|
|||
|
|
html += '</div>';
|
|||
|
|
if (actif) {
|
|||
|
|
html += '<form class="app-chat-fenetre__pied" id="app-chat-form"><input type="text" name="contenu" maxlength="2000" placeholder="Écrire un message…" required><button type="submit">Envoyer</button></form>';
|
|||
|
|
} else {
|
|||
|
|
html += '<p class="app-chat-fenetre__desactive">Le chat est désactivé pour cette classe.</p>';
|
|||
|
|
}
|
|||
|
|
corps.innerHTML = html;
|
|||
|
|
var zone = document.getElementById('app-chat-corps');
|
|||
|
|
zone.scrollTop = zone.scrollHeight;
|
|||
|
|
var form = document.getElementById('app-chat-form');
|
|||
|
|
if (form) form.addEventListener('submit', function (e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
var champ = form.querySelector('input');
|
|||
|
|
var texte = champ.value.trim();
|
|||
|
|
if (!texte) return;
|
|||
|
|
var url = ROLE === 'prof' ? URLS.envoyer.replace('__ID__', filActuel) : URLS.envoyer;
|
|||
|
|
fetch(url, { method: 'POST', headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ contenu: texte }) })
|
|||
|
|
.then(function () { champ.value = ''; sonderConversation(); }).catch(function () {});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function sonderConversation() {
|
|||
|
|
var url = ROLE === 'prof' ? URLS.etat.replace('__ID__', filActuel) : URLS.etat + '?lu=1';
|
|||
|
|
fetch(url, { headers: { Accept: 'application/json' } }).then(function (r) { return r.json(); }).then(function (d) {
|
|||
|
|
rendreMessages(fenetre, d.messages, d.actif);
|
|||
|
|
}).catch(function () {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function rendreEntete(titre, retour) {
|
|||
|
|
return '<div class="app-chat-fenetre__entete">' +
|
|||
|
|
(retour ? '<button type="button" id="app-chat-retour" aria-label="Retour">←</button>' : '') +
|
|||
|
|
'<span style="flex:1">' + echapper(titre) + '</span>' +
|
|||
|
|
'<button type="button" id="app-chat-fermer" aria-label="Fermer">✕</button></div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ouvrirConversation(id, titre) {
|
|||
|
|
filActuel = id;
|
|||
|
|
fenetre.innerHTML = rendreEntete(titre, ROLE === 'prof');
|
|||
|
|
sonderConversation();
|
|||
|
|
attacherFermeture();
|
|||
|
|
if (ROLE === 'prof') document.getElementById('app-chat-retour').addEventListener('click', ouvrirInbox);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ouvrirInbox() {
|
|||
|
|
filActuel = null;
|
|||
|
|
fenetre.innerHTML = rendreEntete('Messages', false) + '<ul class="app-chat-fenetre__liste-fils" id="app-chat-fils"></ul>';
|
|||
|
|
attacherFermeture();
|
|||
|
|
fetch(URLS.index, { headers: { Accept: 'application/json' } }).then(function (r) { return r.json(); }).then(function (d) {
|
|||
|
|
var liste = document.getElementById('app-chat-fils');
|
|||
|
|
if (!liste) return;
|
|||
|
|
if (d.fils.length === 0) { liste.innerHTML = '<li style="padding:10px 6px;color:var(--sy-muted);font-size:13px">Aucune conversation pour l’instant.</li>'; return; }
|
|||
|
|
liste.innerHTML = d.fils.map(function (f) {
|
|||
|
|
return '<li><button type="button" data-fil="' + f.id + '" data-eleve="' + echapper(f.eleve) + '">' + echapper(f.eleve) + ' · ' + echapper(f.classe) +
|
|||
|
|
(f.non_lus ? ' <span class="app-sidebar__badge">' + f.non_lus + '</span>' : '') + '</button></li>';
|
|||
|
|
}).join('');
|
|||
|
|
liste.querySelectorAll('button[data-fil]').forEach(function (b) {
|
|||
|
|
b.addEventListener('click', function () { ouvrirConversation(b.dataset.fil, b.dataset.eleve); });
|
|||
|
|
});
|
|||
|
|
}).catch(function () {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function attacherFermeture() {
|
|||
|
|
document.getElementById('app-chat-fermer').addEventListener('click', fermer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fermer() {
|
|||
|
|
ouverte = false;
|
|||
|
|
fenetre.hidden = true;
|
|||
|
|
fenetre.classList.remove('est-ouverte');
|
|||
|
|
bulle.setAttribute('aria-expanded', 'false');
|
|||
|
|
if (minuteur) clearInterval(minuteur);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ouvrir() {
|
|||
|
|
ouverte = true;
|
|||
|
|
fenetre.hidden = false;
|
|||
|
|
fenetre.classList.add('est-ouverte');
|
|||
|
|
bulle.setAttribute('aria-expanded', 'true');
|
|||
|
|
if (ROLE === 'eleve') ouvrirConversation(null, 'Mon prof'); else ouvrirInbox();
|
|||
|
|
minuteur = setInterval(function () { if (filActuel !== null || ROLE === 'eleve') sonderConversation(); }, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bulle.addEventListener('click', function () { ouverte ? fermer() : ouvrir(); });
|
|||
|
|
|
|||
|
|
function sonderBadge() {
|
|||
|
|
if (ROLE === 'eleve') {
|
|||
|
|
fetch(URLS.etat, { headers: { Accept: 'application/json' } }).then(function (r) { return r.json(); }).then(function (d) {
|
|||
|
|
badge.hidden = !d.non_lus;
|
|||
|
|
badge.textContent = d.non_lus;
|
|||
|
|
}).catch(function () {});
|
|||
|
|
} else {
|
|||
|
|
fetch(URLS.index, { headers: { Accept: 'application/json' } }).then(function (r) { return r.json(); }).then(function (d) {
|
|||
|
|
var total = d.fils.reduce(function (s, f) { return s + f.non_lus; }, 0);
|
|||
|
|
badge.hidden = total === 0;
|
|||
|
|
badge.textContent = total;
|
|||
|
|
}).catch(function () {});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
sonderBadge();
|
|||
|
|
setInterval(function () { if (!ouverte) sonderBadge(); }, 15000);
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
@stack('scripts_bas')
|
|||
|
|
</body>
|
|||
|
|
</html>
|