feat: logo and favicon management in admin panel
CI/CD / CI · dotnet build (push) Successful in 41s
CI/CD / Deploy · drsousan (push) Successful in 29s

Admin panel:
- New 'هویت سایت' page under تنظیمات in sidebar
- Upload logo (PNG transparent, 200×60px recommended)
- Upload favicon (PNG/ICO, 32×32 or 64×64px)
- Live preview panel shows how logo looks in header
  and how favicon looks in a browser tab mockup
- Saved to SiteSettings with section='identity', key='logo'/'favicon'

Frontend (_Layout.cshtml):
- Injects AppDbContext to load identity settings per request
- If logo is set: shows <img> in header instead of text
- If favicon is set: uses uploaded file as <link rel="icon">
- Falls back to text / favicon.ico when not configured

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 17:47:49 +03:30
parent 81838f75ce
commit e79ccf7e8c
2 changed files with 157 additions and 3 deletions
+27 -1
View File
@@ -1,3 +1,12 @@
@using Microsoft.EntityFrameworkCore
@inject DrSousan.Api.Data.AppDbContext _layoutDb
@{
var _identity = await _layoutDb.SiteSettings
.Where(s => s.Section == "identity")
.ToListAsync();
var _logoUrl = _identity.FirstOrDefault(s => s.Key == "logo")?.Value ?? "";
var _faviconUrl = _identity.FirstOrDefault(s => s.Key == "favicon")?.Value ?? "";
}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
@@ -8,7 +17,15 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'" /> <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" rel="stylesheet" /></noscript>
@if (!string.IsNullOrEmpty(_faviconUrl))
{
<link rel="icon" href="@_faviconUrl" type="image/png" />
<link rel="shortcut icon" href="@_faviconUrl" />
}
else
{
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
}
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { :root {
@@ -122,7 +139,16 @@
<body> <body>
<header> <header>
<a class="logo" href="/">@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")</a> <a class="logo" href="/">
@if (!string.IsNullOrEmpty(_logoUrl))
{
<img src="@_logoUrl" alt="@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")" style="height:38px;width:auto;object-fit:contain;vertical-align:middle" />
}
else
{
@(ViewData["SiteName"] ?? "دکتر سوسن آل‌طه")
}
</a>
<nav> <nav>
<a href="/#about">درباره من</a> <a href="/#about">درباره من</a>
<a href="/#services">خدمات</a> <a href="/#services">خدمات</a>
+129 -1
View File
@@ -279,6 +279,10 @@ tr:hover td{background:#FAFBFC}
درخواست‌ها <span id="healthreqBadge" style="display:none;background:#E53935;color:#fff;font-size:.65rem;padding:.1rem .4rem;border-radius:50px;margin-right:.3rem"></span> درخواست‌ها <span id="healthreqBadge" style="display:none;background:#E53935;color:#fff;font-size:.65rem;padding:.1rem .4rem;border-radius:50px;margin-right:.3rem"></span>
</div> </div>
<div class="nav-section">تنظیمات</div> <div class="nav-section">تنظیمات</div>
<div class="nav-item" onclick="showPage('siteidentity',this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
هویت سایت (لوگو / فاویکون)
</div>
<div class="nav-item" onclick="showPage('security',this)"> <div class="nav-item" onclick="showPage('security',this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
تغییر رمز عبور تغییر رمز عبور
@@ -536,6 +540,84 @@ tr:hover td{background:#FAFBFC}
</div> </div>
<!-- ══ SECURITY PAGE ══ --> <!-- ══ SECURITY PAGE ══ -->
<!-- ══ SITE IDENTITY ══ -->
<div class="page" id="page-siteidentity">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.2rem">
<!-- Logo -->
<div class="card">
<div class="card-header"><div class="card-title">🖼️ لوگوی سایت</div></div>
<div class="modal-body">
<p style="font-size:.83rem;color:var(--mid);margin-bottom:1.2rem;line-height:1.8">
لوگو در هدر سایت و صفحات وبلاگ نمایش داده می‌شود.<br>
<strong>فرمت پیشنهادی:</strong> PNG با پس‌زمینه شفاف (transparent) — ابعاد: ۲۰۰×۶۰ پیکسل
</p>
<input type="hidden" id="si-logo"/>
<div class="input-upload-wrap" style="flex-wrap:wrap;gap:.5rem">
<button class="upload-btn" id="upload-btn-si-logo" onclick="uploadImage('si-logo','prev-si-logo')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
آپلود لوگو
</button>
<button type="button" class="upload-remove" id="rm-si-logo" onclick="removeImage('si-logo','prev-si-logo')" style="display:none">حذف</button>
</div>
<img id="prev-si-logo" class="upload-preview" alt="لوگو" style="max-height:80px;object-fit:contain;background:#f5f5f5;padding:.5rem"/>
<div style="margin-top:1.2rem">
<button class="btn btn-primary" onclick="saveSiteIdentity()">ذخیره لوگو</button>
</div>
</div>
</div>
<!-- Favicon -->
<div class="card">
<div class="card-header"><div class="card-title">⭐ فاویکون (Favicon)</div></div>
<div class="modal-body">
<p style="font-size:.83rem;color:var(--mid);margin-bottom:1.2rem;line-height:1.8">
آیکون کوچکی که در تب مرورگر نمایش داده می‌شود.<br>
<strong>فرمت پیشنهادی:</strong> PNG یا ICO — ابعاد: ۳۲×۳۲ یا ۶۴×۶۴ پیکسل
</p>
<input type="hidden" id="si-favicon"/>
<div class="input-upload-wrap" style="flex-wrap:wrap;gap:.5rem">
<button class="upload-btn" id="upload-btn-si-favicon" onclick="uploadImage('si-favicon','prev-si-favicon')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
آپلود فاویکون
</button>
<button type="button" class="upload-remove" id="rm-si-favicon" onclick="removeImage('si-favicon','prev-si-favicon')" style="display:none">حذف</button>
</div>
<img id="prev-si-favicon" class="upload-preview" alt="فاویکون" style="max-height:64px;max-width:64px;object-fit:contain;background:#f5f5f5;padding:.4rem;border-radius:8px"/>
<div style="margin-top:1.2rem">
<button class="btn btn-primary" onclick="saveSiteIdentity()">ذخیره فاویکون</button>
</div>
</div>
</div>
</div>
<!-- Preview -->
<div class="card" style="margin-top:1.2rem">
<div class="card-header"><div class="card-title">پیش‌نمایش</div></div>
<div class="modal-body">
<div style="display:flex;align-items:center;gap:2rem;flex-wrap:wrap">
<div>
<p style="font-size:.75rem;color:var(--light);margin-bottom:.5rem">هدر سایت</p>
<div style="background:#FAFAF7;border:1px solid var(--border);border-radius:10px;padding:.8rem 1.4rem;display:flex;align-items:center;gap:.8rem;min-width:220px">
<img id="preview-logo-header" src="" alt="" style="height:36px;object-fit:contain;display:none"/>
<span id="preview-logo-text" style="font-size:.95rem;font-weight:700;color:#B8955A">دکتر سوسن آل‌طه</span>
</div>
</div>
<div>
<p style="font-size:.75rem;color:var(--light);margin-bottom:.5rem">تب مرورگر</p>
<div style="background:#E8EAED;border-radius:8px 8px 0 0;padding:.4rem .9rem;display:inline-flex;align-items:center;gap:.5rem;font-size:.78rem;color:#333;min-width:180px">
<img id="preview-favicon-tab" src="" alt="" style="width:16px;height:16px;object-fit:contain;display:none"/>
<span id="preview-favicon-text">🌐</span>
<span style="color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">دکتر سوسن آل‌طه</span>
</div>
</div>
</div>
<p style="font-size:.78rem;color:var(--light);margin-top:1rem">⚠️ پس از ذخیره، تغییرات فاویکون ممکن است نیاز به پاک‌کردن کش مرورگر داشته باشد.</p>
</div>
</div>
</div>
<div class="page" id="page-security"> <div class="page" id="page-security">
<div class="card" style="max-width:480px"> <div class="card" style="max-width:480px">
<div class="card-header"><div class="card-title">🔒 تغییر رمز عبور مدیر</div></div> <div class="card-header"><div class="card-title">🔒 تغییر رمز عبور مدیر</div></div>
@@ -1063,7 +1145,7 @@ function toast(msg, type='success') {
} }
// ── Navigation ──────────────────────────────────────────────────────────────── // ── Navigation ────────────────────────────────────────────────────────────────
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دسته‌بندی‌ها',faqs:'سوالات متداول',seo:'گزارش SEO',comments:'مدیریت نظرات',security:'تغییر رمز عبور',patients:'پرونده بیماران','patient-profile':'پرونده بیمار',healthrequests:'درخواست‌های سلامت'}; const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دسته‌بندی‌ها',faqs:'سوالات متداول',seo:'گزارش SEO',comments:'مدیریت نظرات',security:'تغییر رمز عبور',patients:'پرونده بیماران','patient-profile':'پرونده بیمار',healthrequests:'درخواست‌های سلامت',siteidentity:'هویت سایت (لوگو / فاویکون)'};
function showPage(name, el) { function showPage(name, el) {
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
@@ -1089,6 +1171,7 @@ function loadPage(name) {
else if (name==='comments') loadComments(); else if (name==='comments') loadComments();
else if (name==='patients') loadPatients(); else if (name==='patients') loadPatients();
else if (name==='healthrequests') loadHealthRequests(); else if (name==='healthrequests') loadHealthRequests();
else if (name==='siteidentity') loadSiteIdentity();
} }
// ── Patients ────────────────────────────────────────────────────────────────── // ── Patients ──────────────────────────────────────────────────────────────────
@@ -1290,6 +1373,51 @@ async function handleReq(id){await api(`/api/health-requests/${id}`,{method:'PUT
async function deleteReq(id){if(!confirm('حذف؟'))return;await api(`/api/health-requests/${id}`,{method:'DELETE'});toast('حذف شد','error');loadHealthRequests();} async function deleteReq(id){if(!confirm('حذف؟'))return;await api(`/api/health-requests/${id}`,{method:'DELETE'});toast('حذف شد','error');loadHealthRequests();}
// ── Comments ────────────────────────────────────────────────────────────────── // ── Comments ──────────────────────────────────────────────────────────────────
// ── Site Identity (logo / favicon) ────────────────────────────────────────────
async function loadSiteIdentity() {
const data = await api('/api/settings/identity') || [];
const vals = {};
data.forEach(s => vals[s.key] = s.value);
// Set hidden inputs
document.getElementById('si-logo').value = vals.logo || '';
document.getElementById('si-favicon').value = vals.favicon || '';
// Show previews
showPreview('prev-si-logo', vals.logo || '');
showPreview('prev-si-favicon', vals.favicon || '');
// Update live preview panel
updateSiteIdentityPreview(vals.logo || '', vals.favicon || '');
}
function updateSiteIdentityPreview(logoUrl, faviconUrl) {
const logoImg = document.getElementById('preview-logo-header');
const logoTxt = document.getElementById('preview-logo-text');
if (logoUrl) { logoImg.src=logoUrl; logoImg.style.display='block'; logoTxt.style.display='none'; }
else { logoImg.style.display='none'; logoTxt.style.display=''; }
const favImg = document.getElementById('preview-favicon-tab');
const favTxt = document.getElementById('preview-favicon-text');
if (faviconUrl) { favImg.src=faviconUrl; favImg.style.display='inline'; favTxt.style.display='none'; }
else { favImg.style.display='none'; favTxt.style.display=''; }
}
async function saveSiteIdentity() {
const logo = document.getElementById('si-logo').value;
const favicon = document.getElementById('si-favicon').value;
await api('/api/settings/identity', {
method: 'PUT',
body: JSON.stringify({ settings: { logo, favicon } })
});
updateSiteIdentityPreview(logo, favicon);
toast('ذخیره شد ✓ — برای اعمال فاویکون کش مرورگر را پاک کنید');
}
// Live preview while uploading
document.addEventListener('change', () => {
const logo = document.getElementById('si-logo')?.value || '';
const favicon = document.getElementById('si-favicon')?.value || '';
if (logo || favicon) updateSiteIdentityPreview(logo, favicon);
});
async function loadComments() { async function loadComments() {
const all = await api('/api/comments') || []; const all = await api('/api/comments') || [];
const pending = all.filter(c => !c.isApproved); const pending = all.filter(c => !c.isApproved);