feat: logo and favicon management in admin panel
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:
@@ -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>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
@@ -8,7 +17,15 @@
|
||||
<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'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
@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" />
|
||||
}
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
@@ -122,7 +139,16 @@
|
||||
<body>
|
||||
|
||||
<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>
|
||||
<a href="/#about">درباره من</a>
|
||||
<a href="/#services">خدمات</a>
|
||||
|
||||
@@ -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>
|
||||
</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)">
|
||||
<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>
|
||||
|
||||
<!-- ══ 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="card" style="max-width:480px">
|
||||
<div class="card-header"><div class="card-title">🔒 تغییر رمز عبور مدیر</div></div>
|
||||
@@ -1063,7 +1145,7 @@ function toast(msg, type='success') {
|
||||
}
|
||||
|
||||
// ── 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) {
|
||||
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
||||
@@ -1089,6 +1171,7 @@ function loadPage(name) {
|
||||
else if (name==='comments') loadComments();
|
||||
else if (name==='patients') loadPatients();
|
||||
else if (name==='healthrequests') loadHealthRequests();
|
||||
else if (name==='siteidentity') loadSiteIdentity();
|
||||
}
|
||||
|
||||
// ── 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();}
|
||||
|
||||
// ── 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() {
|
||||
const all = await api('/api/comments') || [];
|
||||
const pending = all.filter(c => !c.isApproved);
|
||||
|
||||
Reference in New Issue
Block a user