fix: cropper mime bug + loadSiteIdentity crash + logo|name header
CI/CD / CI · dotnet build (push) Successful in 37s
CI/CD / Deploy · drsousan (push) Successful in 29s

1. applyCrop() — mime variable was declared INSIDE toBlob callback
   but used as an argument to toBlob() (outer scope) → ReferenceError.
   Fix: declare _mime, _quality, _ext BEFORE out.toBlob() call.

2. loadSiteIdentity() — crashed when identity section had no rows
   (data was null/non-array). Fix: safe Array.isArray guard + catch.

3. Header logo: show logo image + | + site name side by side
   when logo is configured (was showing one OR the other).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 18:15:51 +03:30
parent 5d6a4a630d
commit 1e51df406b
2 changed files with 15 additions and 16 deletions
+4 -6
View File
@@ -139,15 +139,13 @@
<body> <body>
<header> <header>
<a class="logo" href="/"> <a class="logo" href="/" style="display:flex;align-items:center;gap:.6rem">
@if (!string.IsNullOrEmpty(_logoUrl)) @if (!string.IsNullOrEmpty(_logoUrl))
{ {
<img src="@_logoUrl" alt="@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")" style="height:38px;width:auto;object-fit:contain;vertical-align:middle" /> <img src="@_logoUrl" alt="@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")" style="height:36px;width:auto;object-fit:contain;flex-shrink:0" />
} <span style="color:var(--border);font-weight:300;font-size:1.1rem;line-height:1">|</span>
else
{
@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")
} }
<span>@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")</span>
</a> </a>
<nav> <nav>
<a href="/#about">درباره من</a> <a href="/#about">درباره من</a>
+11 -10
View File
@@ -1375,9 +1375,9 @@ async function deleteReq(id){if(!confirm('حذف؟'))return;await api(`/api/heal
// ── Comments ────────────────────────────────────────────────────────────────── // ── Comments ──────────────────────────────────────────────────────────────────
// ── Site Identity (logo / favicon) ──────────────────────────────────────────── // ── Site Identity (logo / favicon) ────────────────────────────────────────────
async function loadSiteIdentity() { async function loadSiteIdentity() {
const data = await api('/api/settings/identity') || []; const data = await api('/api/settings/identity').catch(()=>[]) || [];
const vals = {}; const vals = {};
data.forEach(s => vals[s.key] = s.value); (Array.isArray(data) ? data : []).forEach(s => vals[s.key] = s.value);
// Set hidden inputs // Set hidden inputs
document.getElementById('si-logo').value = vals.logo || ''; document.getElementById('si-logo').value = vals.logo || '';
document.getElementById('si-favicon').value = vals.favicon || ''; document.getElementById('si-favicon').value = vals.favicon || '';
@@ -1823,8 +1823,13 @@ async function applyCrop() {
const out = document.createElement('canvas'); const out = document.createElement('canvas');
out.width = Math.round(sw); out.height = Math.round(sh); out.width = Math.round(sw); out.height = Math.round(sh);
out.getContext('2d').drawImage(cropper.img, sx, sy, sw, sh, 0, 0, out.width, out.height); out.getContext('2d').drawImage(cropper.img, sx, sy, sw, sh, 0, 0, out.width, out.height);
// Capture targets BEFORE closing (closeCropper doesn't clear them, but be safe) // Capture targets and mime type BEFORE the async callback
const _inputId = cropper.inputId, _previewId = cropper.previewId; const _inputId = cropper.inputId;
const _previewId = cropper.previewId;
const _mime = cropper.mimeType || 'image/jpeg';
const _quality = (_mime === 'image/jpeg' || _mime === 'image/webp') ? 0.92 : undefined;
const _ext = _mime.split('/')[1].replace('jpeg','jpg').replace('vnd.microsoft.icon','ico').replace('x-icon','ico').replace('svg+xml','svg');
out.toBlob(async blob => { out.toBlob(async blob => {
closeCropper(); closeCropper();
const inputId = _inputId, previewId = _previewId; const inputId = _inputId, previewId = _previewId;
@@ -1834,8 +1839,7 @@ async function applyCrop() {
btn.disabled = true; btn.disabled = true;
try { try {
const fd = new FormData(); const fd = new FormData();
const ext = _inputId && cropper.mimeType ? cropper.mimeType.split('/')[1].replace('jpeg','jpg') : 'jpg'; fd.append('file', new File([blob], `crop.${_ext}`, {type: _mime}));
fd.append('file', new File([blob], `crop.${ext}`, {type: cropper.mimeType || 'image/jpeg'}));
const r = await fetch('/api/upload', {method:'POST', headers:{'Authorization':`Bearer ${token}`}, body:fd}); const r = await fetch('/api/upload', {method:'POST', headers:{'Authorization':`Bearer ${token}`}, body:fd});
if (!r.ok) throw new Error(await r.text()); if (!r.ok) throw new Error(await r.text());
const { url } = await r.json(); const { url } = await r.json();
@@ -1844,10 +1848,7 @@ async function applyCrop() {
toast('تصویر با موفقیت آپلود شد ✓'); toast('تصویر با موفقیت آپلود شد ✓');
} catch(e) { toast('خطا در آپلود: '+e.message,'error'); } } catch(e) { toast('خطا در آپلود: '+e.message,'error'); }
finally { btn.innerHTML=orig; btn.disabled=false; } finally { btn.innerHTML=orig; btn.disabled=false; }
// Use original mime type; only pass quality for lossy formats }, _mime, _quality);
const mime = cropper.mimeType || 'image/jpeg';
const quality = (mime === 'image/jpeg' || mime === 'image/webp') ? 0.92 : undefined;
}, mime, quality);
} }
async function uploadImage(inputId, previewId) { async function uploadImage(inputId, previewId) {