feat(journal): карточка отчёта по ученику в модалке

- новая модалка #reportCardModal с фото, именем, текстом записи и бейджами
- кнопка «Карточка отчёта» (иконка contact) в плитке и в карточке журнала
- закрытие по клику по оверлею и по Escape
- imgModal поднят над карточкой, чтобы просмотр фото открывался поверх неё
This commit is contained in:
dev
2026-10-04 21:53:05 +03:00
parent 19be1cc9ef
commit 8fc6ea804a
3 changed files with 217 additions and 0 deletions
+109
View File
@@ -223,6 +223,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
.modal-overlay.open{display:flex} .modal-overlay.open{display:flex}
.modal-overlay > img{max-width:90%;max-height:90dvh;border-radius:var(--radius)} .modal-overlay > img{max-width:90%;max-height:90dvh;border-radius:var(--radius)}
#videoModal{background:rgba(0,0,0,.85)} #videoModal{background:rgba(0,0,0,.85)}
#imgModal{z-index:130}
.video-modal{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;width:min(1000px,94vw);max-height:94dvh;display:flex;flex-direction:column;gap:12px} .video-modal{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;width:min(1000px,94vw);max-height:94dvh;display:flex;flex-direction:column;gap:12px}
.video-head{display:flex;align-items:center;gap:12px;font-size:.85rem;color:var(--text)} .video-head{display:flex;align-items:center;gap:12px;font-size:.85rem;color:var(--text)}
.video-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .video-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -704,4 +705,112 @@ body[data-page=settings] .page-head{align-items:flex-end}
.lesson-dash-when{color:var(--muted);font-size:.74rem;margin-left:auto;white-space:nowrap} .lesson-dash-when{color:var(--muted);font-size:.74rem;margin-left:auto;white-space:nowrap}
.lesson-dash-text{font-size:.8rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.45} .lesson-dash-text{font-size:.8rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.45}
/* --- Карточка отчёта об ученике (модалка на journal.html) --- */
.report-overlay{background:rgba(0,0,0,.62);backdrop-filter:blur(10px) saturate(1.08);-webkit-backdrop-filter:blur(10px) saturate(1.08);padding:24px 16px;overflow:auto;overscroll-behavior:contain}
.student-card{
--sc-primary:#3478F6;
--sc-primary-dark:#173B78;
--sc-primary-soft:#E8F2FF;
--sc-text:#172033;
--sc-text-secondary:#68758A;
--sc-border:#E5EBF3;
--sc-yellow:#FFB71B;
--sc-radius-card:20px;
--sc-radius-photo:18px;
--sc-radius-pill:999px;
--sc-pad-x:22px;
--sc-media-inset:20px;
--sc-smiley-size:50px;
--sc-smiley-bottom:16px;
position:relative;
width:100%;
max-width:440px;
background:var(--card);
color:var(--sc-text);
border-radius:var(--sc-radius-card);
box-shadow:0 10px 30px rgba(39,91,160,.10),0 2px 8px rgba(39,91,160,.06),0 24px 60px rgba(0,0,0,.35);
overflow:hidden;
flex-shrink:0;
-webkit-tap-highlight-color:transparent;
}
.student-card__media{
position:relative;
margin:var(--sc-media-inset) var(--sc-media-inset) 0;
aspect-ratio:1.55/1;
border-radius:var(--sc-radius-photo);
overflow:hidden;
background:var(--sc-primary-soft);
rotate:-3deg;
transform-origin:50% 50%;
}
.student-card__photo{display:block;width:100%;height:100%;object-fit:cover;filter:saturate(1.04) contrast(1.02) brightness(1.02);cursor:zoom-in}
.student-card__photo[hidden]{display:none}
.student-card__media::after{
content:"";
position:absolute;
inset:auto 0 0;
height:25%;
background:linear-gradient(to bottom,transparent,rgba(10,25,50,.08));
pointer-events:none;
}
.student-card__nophoto{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
color:#A9BEDC;
}
.student-card__nophoto[hidden]{display:none}
.student-card__nophoto svg{width:44%;height:44%}
.student-card__content{
position:relative;
padding:24px var(--sc-pad-x) calc(var(--sc-smiley-bottom) + var(--sc-smiley-size) + 18px);
}
.student-card__sparks{position:absolute;top:15px;right:16px;width:104px;color:#E1ECFB;pointer-events:none}
.student-card__name{margin:0 0 8px;font-size:20px;font-weight:750;line-height:1.25;letter-spacing:-.1px;color:var(--sc-text);overflow-wrap:break-word}
.student-card__text{margin:0;font-size:15px;line-height:1.6;color:var(--sc-text-secondary);white-space:pre-wrap;overflow-wrap:break-word}
.student-card__text:empty{display:none}
.student-card__smiley{position:absolute;right:24px;bottom:var(--sc-smiley-bottom);width:var(--sc-smiley-size);height:var(--sc-smiley-size);pointer-events:none}
.student-card__footer{
display:flex;
align-items:center;
padding:14px var(--sc-pad-x);
border-top:1px solid var(--sc-border);
background:linear-gradient(to bottom,#FBFDFF 0%,#FFFFFF 100%);
}
.student-card__footer-badges{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;min-width:0}
.student-card__badge{
display:inline-flex;
align-items:center;
gap:7px;
max-width:100%;
padding:8px 15px;
border-radius:var(--sc-radius-pill);
font-size:14px;
font-weight:700;
line-height:1;
letter-spacing:.1px;
color:#FFFFFF;
white-space:nowrap;
user-select:none;
}
.student-card__badge--time{background:rgba(23,59,120,.94)}
.student-card__badge--tool{background:var(--sc-primary);box-shadow:0 4px 12px rgba(52,120,246,.28)}
.student-card__badge--tool[hidden]{display:none}
.student-card__badge > span{overflow:hidden;text-overflow:ellipsis}
.student-card__icon{display:block;flex:none}
.student-card__icon--badge{width:17px;height:17px}
.student-card__icon--ps{width:19px;height:19px}
.student-card :focus-visible{outline:2px solid var(--sc-primary);outline-offset:2px;border-radius:6px}
.journal-card .foot .edit svg,.card .edit svg{width:16px;height:16px;display:block}
@media(max-width:420px){
.report-overlay{padding:16px 12px}
.student-card{
--sc-pad-x:15px;
--sc-media-inset:14px;
--sc-smiley-size:40px;
}
.student-card__badge{font-size:12px;padding:6px 11px}
}
+63
View File
@@ -263,6 +263,69 @@
</div> </div>
</div> </div>
<div class="modal-overlay report-overlay" id="reportCardModal">
<article class="student-card" aria-labelledby="reportCardName">
<div class="student-card__media">
<img class="student-card__photo" id="reportCardPhoto" alt="" decoding="async">
<div class="student-card__nophoto" id="reportCardNoPhoto" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<rect x="3" y="4" width="18" height="16" rx="3"/>
<path d="M3 15.5l4.5-4.2 3.5 3.2 3.2-2.8L21 15.5"/>
<circle cx="9" cy="9.2" r="1.4"/>
</svg>
</div>
</div>
<div class="student-card__content">
<svg class="student-card__sparks" viewBox="0 0 120 40" aria-hidden="true" focusable="false">
<g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
<path d="M6 30c14-6 30-6 44 0"/>
<path d="M74 16h16M82 8v16"/>
<path d="M104 6l3 6M104 26l3-6"/>
</g>
</svg>
<h2 class="student-card__name" id="reportCardName"></h2>
<p class="student-card__text" id="reportCardText"></p>
<svg class="student-card__smiley" viewBox="0 0 72 72"
fill="none" stroke="#FFB71B" stroke-width="2.6"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true" focusable="false">
<path d="M38 5.6v5.4M60.6 15l-3.8 3.8M68.4 38H63M60.6 61l-3.8-3.8M38 70.4V65M15.4 61l3.8-3.8M7.6 38H13M15.4 15l3.8 3.8"/>
<path d="M57.6 30.8c1.9 4.4 2.7 9 2.2 13.5-.9 8-6 15-13.7 17.4-7.7 2.4-15.9.6-20.9-5-5-5.6-6.6-14-4-21.2 2.6-7.2 8.8-12.6 16.4-14.3 7.7-1.7 15.9 1.8 20 9.6Z"/>
<path d="M30.6 33.2h.02M45.4 32.4h.02" stroke-width="3.6"/>
<path d="M28.6 45c2.6 4 6.6 6.3 11 6.3 4.5 0 8.6-2.3 11.3-6.4"/>
</svg>
</div>
<footer class="student-card__footer">
<div class="student-card__footer-badges">
<span class="student-card__badge student-card__badge--time">
<svg class="student-card__icon student-card__icon--badge" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.9"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true" focusable="false">
<path d="M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19"/>
<circle cx="10" cy="8" r="3.4"/>
<path d="M20 19v-1.5a3.5 3.5 0 0 0-2.6-3.38"/>
<path d="M15.4 4.6a3.4 3.4 0 0 1 0 6.4"/>
</svg>
<span id="reportCardTime"></span>
</span>
<span class="student-card__badge student-card__badge--tool">
<svg class="student-card__icon student-card__icon--ps" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.9"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true" focusable="false">
<rect x="2.2" y="2.2" width="19.6" height="19.6" rx="5.8" fill="currentColor" fill-opacity=".16"/>
<text x="12" y="16.9" text-anchor="middle" font-size="11.6" font-weight="800"
letter-spacing="-.3" fill="currentColor" stroke="none"
font-family="inherit">Ps</text>
</svg>
<span id="reportCardModule"></span>
</span>
</div>
</footer>
</article>
</div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script src="vendor/lucide.min.js"></script> <script src="vendor/lucide.min.js"></script>
<script src="js/datetime.js"></script> <script src="js/datetime.js"></script>
+45
View File
@@ -48,6 +48,42 @@ function mainPhotoPath(e) {
return null; return null;
} }
function openReportCard(id) {
const e = currentEntries.find(x => x.id === id);
if (!e) return;
const mp = mainPhotoPath(e);
const photo = document.getElementById('reportCardPhoto');
const noPhoto = document.getElementById('reportCardNoPhoto');
if (mp) {
photo.src = thumbSrc(mp);
photo.dataset.img = `${API}${mp}`;
photo.alt = e.student_name || '';
photo.hidden = false;
noPhoto.hidden = true;
} else {
photo.src = '';
delete photo.dataset.img;
photo.alt = '';
photo.hidden = true;
noPhoto.hidden = false;
}
document.getElementById('reportCardName').textContent = e.student_name || '';
document.getElementById('reportCardText').textContent = e.description || '';
const groupBadge = document.getElementById('reportCardTime');
const group = e.group_name || '';
groupBadge.textContent = group;
groupBadge.parentElement.hidden = !group;
const toolBadge = document.getElementById('reportCardModule').parentElement;
const tool = e.module_name || '';
document.getElementById('reportCardModule').textContent = tool;
toolBadge.hidden = !tool;
document.getElementById('reportCardModal').classList.add('open');
}
function closeReportCard() {
document.getElementById('reportCardModal').classList.remove('open');
}
function filesHTML(files) { function filesHTML(files) {
if (!files || !files.length) return ''; if (!files || !files.length) return '';
return `<div class="entry-files">${files.map(f => { return `<div class="entry-files">${files.map(f => {
@@ -126,6 +162,7 @@ function gridEntryHTML(e) {
${filesHTML(e.files)} ${filesHTML(e.files)}
<div class="time">${fmtFull(e.created_at)}</div> <div class="time">${fmtFull(e.created_at)}</div>
</div> </div>
<button class="edit" data-report="${e.id}" title="Карточка отчёта"><i data-lucide="contact"></i></button>
<button class="edit" data-edit="${e.id}" title="Редактировать">✎</button> <button class="edit" data-edit="${e.id}" title="Редактировать">✎</button>
${mp ? `<button class="edit" data-enhance="${e.id}" title="Улучшить фото">✦</button>` : ''} ${mp ? `<button class="edit" data-enhance="${e.id}" title="Улучшить фото">✦</button>` : ''}
<button class="del" data-del="${e.id}" title="Удалить">&times;</button> <button class="del" data-del="${e.id}" title="Удалить">&times;</button>
@@ -150,6 +187,7 @@ function cardEntryHTML(e) {
<div class="foot"> <div class="foot">
<span>${fmtFull(e.created_at)}</span> <span>${fmtFull(e.created_at)}</span>
<span> <span>
<button class="edit" data-report="${e.id}" title="Карточка отчёта"><i data-lucide="contact"></i></button>
<button class="edit" data-edit="${e.id}" title="Редактировать">✎</button> <button class="edit" data-edit="${e.id}" title="Редактировать">✎</button>
${mp ? `<button class="edit" data-enhance="${e.id}" title="Улучшить фото">✦</button>` : ''} ${mp ? `<button class="edit" data-enhance="${e.id}" title="Улучшить фото">✦</button>` : ''}
<button class="del" data-del="${e.id}" title="Удалить">&times;</button> <button class="del" data-del="${e.id}" title="Удалить">&times;</button>
@@ -1514,10 +1552,17 @@ function copyLink() { copyText(createdLinkUrl); }
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open')); document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); }); document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
document.getElementById('linkModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeLinkCreate(); }); document.getElementById('linkModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeLinkCreate(); });
document.getElementById('reportCardModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeReportCard(); });
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (document.getElementById('reportCardModal').classList.contains('open')) closeReportCard();
});
document.getElementById('grid').addEventListener('click', e => { document.getElementById('grid').addEventListener('click', e => {
const det = e.target.closest('[data-detach]'); const det = e.target.closest('[data-detach]');
if (det) { detachFile(parseInt(det.dataset.detach, 10)); return; } if (det) { detachFile(parseInt(det.dataset.detach, 10)); return; }
const rp = e.target.closest('[data-report]');
if (rp) { openReportCard(parseInt(rp.dataset.report, 10)); return; }
const ed = e.target.closest('[data-edit]'); const ed = e.target.closest('[data-edit]');
if (ed) { openEdit(parseInt(ed.dataset.edit, 10)); return; } if (ed) { openEdit(parseInt(ed.dataset.edit, 10)); return; }
const en = e.target.closest('[data-enhance]'); const en = e.target.closest('[data-enhance]');