Paillasse/portail/resources/views/layouts/app.blade.php

309 lines
17 KiB
PHP
Raw Permalink Normal View History

<!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>
@if (! $estProf)
<a href="{{ route('mes-fichiers') }}" role="menuitem">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3.5" y="7" width="17" height="13" rx="2"/><path d="M3.5 7l1.5-3h6l1.5 3"/></svg>
Mes fichiers
</a>
@endif
<a href="{{ route('recherche') }}" role="menuitem">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5 21 21"/></svg>
Recherche
</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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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>