first commit
This commit is contained in:
@@ -0,0 +1,915 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>پنل مدیریت | دکتر سوسن آلطه</title>
|
||||
<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'"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--gold:#B8955A;--gold-l:#D4B483;--gold-pale:#F5ECD8;
|
||||
--bg:#F4F6F8;--white:#fff;--dark:#1E1E1E;--mid:#5A5A5A;
|
||||
--light:#9A9A9A;--border:#E2E8F0;--danger:#E53935;--success:#2E7D32;
|
||||
--sidebar:240px;
|
||||
}
|
||||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--dark);direction:rtl;display:flex;min-height:100vh}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar{width:var(--sidebar);background:var(--dark);color:#fff;display:flex;flex-direction:column;flex-shrink:0;position:fixed;top:0;right:0;bottom:0;z-index:50}
|
||||
.sidebar-logo{padding:1.5rem 1.2rem;border-bottom:1px solid rgba(255,255,255,.08);font-size:.9rem;font-weight:600;color:var(--gold-l);line-height:1.4}
|
||||
.sidebar-logo span{display:block;font-size:.7rem;color:rgba(255,255,255,.4);font-weight:400;margin-top:.2rem}
|
||||
nav.sidebar-nav{flex:1;overflow-y:auto;padding:.8rem 0}
|
||||
.nav-section{font-size:.65rem;font-weight:600;letter-spacing:.08em;color:rgba(255,255,255,.3);padding:.8rem 1.2rem .3rem;text-transform:uppercase}
|
||||
.nav-item{display:flex;align-items:center;gap:.7rem;padding:.65rem 1.2rem;color:rgba(255,255,255,.65);cursor:pointer;font-size:.85rem;transition:background .2s,color .2s;border-right:3px solid transparent}
|
||||
.nav-item:hover{background:rgba(255,255,255,.05);color:#fff}
|
||||
.nav-item.active{background:rgba(184,149,90,.12);color:var(--gold-l);border-right-color:var(--gold)}
|
||||
.nav-item svg{width:16px;height:16px;flex-shrink:0;opacity:.7}
|
||||
.nav-item.active svg{opacity:1}
|
||||
.sidebar-footer{padding:1rem 1.2rem;border-top:1px solid rgba(255,255,255,.08);font-size:.75rem;color:rgba(255,255,255,.35)}
|
||||
|
||||
/* ── Main ── */
|
||||
.main{margin-right:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
|
||||
.topbar{background:var(--white);border-bottom:1px solid var(--border);padding:.75rem 2rem;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:40}
|
||||
.topbar-title{font-size:1rem;font-weight:600;color:var(--dark)}
|
||||
.topbar-actions{display:flex;gap:.7rem;align-items:center}
|
||||
.page-content{padding:2rem;flex:1}
|
||||
|
||||
/* ── Cards ── */
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.2rem;margin-bottom:2rem}
|
||||
.stat-card{background:var(--white);border-radius:14px;padding:1.4rem 1.6rem;border:1px solid var(--border);display:flex;flex-direction:column;gap:.4rem}
|
||||
.stat-card .label{font-size:.78rem;color:var(--light)}
|
||||
.stat-card .value{font-size:2rem;font-weight:700;color:var(--dark)}
|
||||
.stat-card .icon{width:38px;height:38px;border-radius:10px;background:var(--gold-pale);display:flex;align-items:center;justify-content:center;margin-bottom:.4rem}
|
||||
.stat-card .icon svg{width:18px;height:18px;color:var(--gold)}
|
||||
|
||||
.card{background:var(--white);border-radius:14px;border:1px solid var(--border);overflow:hidden;margin-bottom:1.5rem}
|
||||
.card-header{padding:1.2rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.card-title{font-size:.95rem;font-weight:600}
|
||||
.card-body{padding:1.5rem}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.1rem;border-radius:8px;font-family:inherit;font-size:.85rem;font-weight:500;cursor:pointer;border:1.5px solid transparent;transition:all .2s}
|
||||
.btn-primary{background:var(--gold);color:#fff;border-color:var(--gold)}
|
||||
.btn-primary:hover{background:var(--gold-l)}
|
||||
.btn-secondary{background:transparent;color:var(--mid);border-color:var(--border)}
|
||||
.btn-secondary:hover{border-color:var(--gold);color:var(--gold)}
|
||||
.btn-danger{background:transparent;color:var(--danger);border-color:var(--danger)}
|
||||
.btn-danger:hover{background:var(--danger);color:#fff}
|
||||
.btn-sm{padding:.35rem .7rem;font-size:.78rem}
|
||||
.btn svg{width:15px;height:15px}
|
||||
|
||||
/* ── Table ── */
|
||||
.table-wrap{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:.85rem}
|
||||
th{background:#F8FAFC;padding:.75rem 1rem;text-align:right;font-weight:600;color:var(--mid);font-size:.78rem;border-bottom:1px solid var(--border)}
|
||||
td{padding:.75rem 1rem;border-bottom:1px solid var(--border);color:var(--dark);vertical-align:middle}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#FAFBFC}
|
||||
.badge{display:inline-flex;align-items:center;padding:.2rem .65rem;border-radius:50px;font-size:.72rem;font-weight:600}
|
||||
.badge-green{background:#E8F5E9;color:#2E7D32}
|
||||
.badge-red{background:#FFEBEE;color:#C62828}
|
||||
.badge-gold{background:var(--gold-pale);color:var(--gold)}
|
||||
|
||||
/* ── Forms ── */
|
||||
.form-grid{display:grid;gap:1rem}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.form-group{display:flex;flex-direction:column;gap:.35rem}
|
||||
.form-group label{font-size:.82rem;font-weight:500;color:var(--dark)}
|
||||
.form-group input,.form-group select,.form-group textarea{
|
||||
border:1.5px solid var(--border);border-radius:8px;padding:.65rem .9rem;
|
||||
font-family:inherit;font-size:.88rem;color:var(--dark);direction:rtl;
|
||||
transition:border-color .2s,box-shadow .2s;outline:none;background:var(--white)
|
||||
}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(184,149,90,.1)}
|
||||
.form-group textarea{resize:vertical;min-height:100px}
|
||||
.form-hint{font-size:.72rem;color:var(--light);margin-top:.2rem}
|
||||
.seo-score{display:flex;align-items:center;gap:.5rem;padding:.6rem 1rem;border-radius:8px;font-size:.8rem}
|
||||
.seo-score.good{background:#E8F5E9;color:#2E7D32}
|
||||
.seo-score.ok{background:#FFF3E0;color:#E65100}
|
||||
.seo-score.bad{background:#FFEBEE;color:#C62828}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;display:flex;align-items:center;justify-content:center;padding:1rem}
|
||||
.modal-overlay.hidden{display:none}
|
||||
.modal{background:var(--white);border-radius:16px;width:100%;max-width:720px;max-height:90vh;overflow-y:auto}
|
||||
.modal-header{padding:1.2rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--white);z-index:1}
|
||||
.modal-title{font-size:1rem;font-weight:600}
|
||||
.modal-close{background:none;border:none;cursor:pointer;padding:.4rem;color:var(--light)}
|
||||
.modal-body{padding:1.5rem}
|
||||
.modal-footer{padding:1rem 1.5rem;border-top:1px solid var(--border);display:flex;gap:.7rem;justify-content:flex-start}
|
||||
|
||||
/* ── Toast ── */
|
||||
.toast-container{position:fixed;bottom:1.5rem;left:1.5rem;z-index:300;display:flex;flex-direction:column;gap:.5rem}
|
||||
.toast{background:var(--dark);color:#fff;padding:.7rem 1.2rem;border-radius:10px;font-size:.85rem;animation:slideIn .3s ease;box-shadow:0 4px 20px rgba(0,0,0,.2)}
|
||||
.toast.success{border-left:4px solid #4CAF50}
|
||||
.toast.error{border-left:4px solid var(--danger)}
|
||||
@keyframes slideIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* ── Rich text editor minimal ── */
|
||||
.editor-toolbar{border:1.5px solid var(--border);border-bottom:none;border-radius:8px 8px 0 0;padding:.4rem .6rem;display:flex;gap:.3rem;flex-wrap:wrap;background:#F8FAFC}
|
||||
.editor-toolbar button{background:none;border:none;padding:.3rem .5rem;border-radius:4px;cursor:pointer;font-size:.82rem;font-family:inherit;color:var(--mid)}
|
||||
.editor-toolbar button:hover{background:var(--gold-pale);color:var(--gold)}
|
||||
.editor-content{border:1.5px solid var(--border);border-radius:0 0 8px 8px;padding:.8rem 1rem;min-height:250px;outline:none;font-size:.9rem;line-height:1.8;direction:rtl}
|
||||
|
||||
/* ── Pages ── */
|
||||
.page{display:none}
|
||||
.page.active{display:block}
|
||||
|
||||
/* ── Login ── */
|
||||
.login-screen{position:fixed;inset:0;background:var(--dark);display:flex;align-items:center;justify-content:center;z-index:1000}
|
||||
.login-box{background:var(--white);border-radius:20px;padding:2.5rem;width:380px;text-align:center}
|
||||
.login-logo{font-size:1.1rem;font-weight:700;color:var(--gold);margin-bottom:.3rem}
|
||||
.login-sub{font-size:.8rem;color:var(--light);margin-bottom:2rem}
|
||||
.login-screen.hidden{display:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ══ LOGIN ══ -->
|
||||
<div class="login-screen" id="loginScreen">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">دکتر سوسن آلطه</div>
|
||||
<div class="login-sub">پنل مدیریت محتوا</div>
|
||||
<div class="form-group" style="margin-bottom:1rem">
|
||||
<label>نام کاربری</label>
|
||||
<input type="text" id="loginUser" value="admin" placeholder="نام کاربری"/>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:1.5rem">
|
||||
<label>رمز عبور</label>
|
||||
<input type="password" id="loginPass" value="admin123" placeholder="رمز عبور"/>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width:100%;justify-content:center" onclick="doLogin()">ورود به پنل</button>
|
||||
<p id="loginErr" style="color:var(--danger);font-size:.8rem;margin-top:.8rem;display:none">نام کاربری یا رمز عبور اشتباه است</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SIDEBAR ══ -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-logo">مدیریت سایت<span>دکتر سوسن آلطه</span></div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">داشبورد</div>
|
||||
<div class="nav-item active" onclick="showPage('dashboard',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
داشبورد
|
||||
</div>
|
||||
<div class="nav-section">محتوای سایت</div>
|
||||
<div class="nav-item" onclick="showPage('hero',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
||||
صفحه اصلی (هرو)
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('about',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>
|
||||
درباره من
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('contact',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
اطلاعات تماس
|
||||
</div>
|
||||
<div class="nav-section">بخشهای سایت</div>
|
||||
<div class="nav-item" onclick="showPage('services',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||||
خدمات
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('gallery',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
گالری
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('testimonials',this)">
|
||||
<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>
|
||||
نظرات بیماران
|
||||
</div>
|
||||
<div class="nav-section">وبلاگ و SEO</div>
|
||||
<div class="nav-item" onclick="showPage('blogposts',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
مقالات
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('categories',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
دستهبندیها
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('seo',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
گزارش SEO
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">API: <span id="apiLabel">localhost:5000</span></div>
|
||||
</aside>
|
||||
|
||||
<!-- ══ MAIN ══ -->
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" id="pageTitle">داشبورد</div>
|
||||
<div class="topbar-actions">
|
||||
<a href="../index.html" target="_blank" class="btn btn-secondary btn-sm">مشاهده سایت</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="logout()">خروج</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ── DASHBOARD ── -->
|
||||
<div class="page active" id="page-dashboard">
|
||||
<div class="stat-grid" id="dashStats">
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div><div class="label">مقالات منتشرشده</div><div class="value" id="ds-posts">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></div><div class="label">کل بازدید مقالات</div><div class="value" id="ds-views">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div><div class="label">نظرات بیماران</div><div class="value" id="ds-testimonials">-</div></div>
|
||||
<div class="stat-card"><div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></div><div class="label">مقالات بدون متا</div><div class="value" id="ds-nometa" style="color:var(--danger)">-</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">پربازدیدترین مقالات</div></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>عنوان</th><th>بازدید</th><th>عملیات</th></tr></thead><tbody id="topPostsTable"></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── HERO ── -->
|
||||
<div class="page" id="page-hero">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">ویرایش بخش هرو</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نام دکتر</label><input id="hero-name"/></div>
|
||||
<div class="form-group"><label>تخصص (زیر نام)</label><input id="hero-title"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>توضیح کوتاه</label><textarea id="hero-subtitle" rows="2"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۱ - عدد</label><input id="hero-stat1_num"/></div>
|
||||
<div class="form-group"><label>آمار ۱ - برچسب</label><input id="hero-stat1_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۲ - عدد</label><input id="hero-stat2_num"/></div>
|
||||
<div class="form-group"><label>آمار ۲ - برچسب</label><input id="hero-stat2_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آمار ۳ - عدد</label><input id="hero-stat3_num"/></div>
|
||||
<div class="form-group"><label>آمار ۳ - برچسب</label><input id="hero-stat3_lbl"/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>دکمه اصلی</label><input id="hero-cta_primary"/></div>
|
||||
<div class="form-group"><label>دکمه ثانویه</label><input id="hero-cta_secondary"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('hero')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── ABOUT ── -->
|
||||
<div class="page" id="page-about">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">ویرایش بخش درباره من</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>عنوان</label><input id="about-title"/></div>
|
||||
<div class="form-group"><label>سالهای تجربه</label><input id="about-years_exp"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>بیوگرافی</label><textarea id="about-bio" rows="4"></textarea></div>
|
||||
<div class="form-group"><label>دستاورد ۱</label><input id="about-cred1"/></div>
|
||||
<div class="form-group"><label>دستاورد ۲</label><input id="about-cred2"/></div>
|
||||
<div class="form-group"><label>دستاورد ۳</label><input id="about-cred3"/></div>
|
||||
<div class="form-group"><label>دستاورد ۴</label><input id="about-cred4"/></div>
|
||||
<div class="form-group"><label>دستاورد ۵</label><input id="about-cred5"/></div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('about')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CONTACT ── -->
|
||||
<div class="page" id="page-contact">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">اطلاعات تماس</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>تلفن</label><input id="contact-phone"/></div>
|
||||
<div class="form-group"><label>ایمیل</label><input id="contact-email"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>آدرس</label><input id="contact-address"/></div>
|
||||
<div class="form-group"><label>ساعات کاری</label><input id="contact-hours"/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>لینک اینستاگرام</label><input id="contact-instagram" dir="ltr"/></div>
|
||||
<div class="form-group"><label>لینک واتساپ</label><input id="contact-whatsapp" dir="ltr"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>لینک تلگرام</label><input id="contact-telegram" dir="ltr"/></div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem"><button class="btn btn-primary" onclick="saveSection('contact')">ذخیره تغییرات</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SERVICES ── -->
|
||||
<div class="page" id="page-services">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">خدمات</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openSvcModal()">+ افزودن خدمت</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>#</th><th>عنوان</th><th>توضیح</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="servicesTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── GALLERY ── -->
|
||||
<div class="page" id="page-gallery">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">گالری</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openGalleryModal()">+ افزودن تصویر</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>دسته</th><th>توضیح</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="galleryTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TESTIMONIALS ── -->
|
||||
<div class="page" id="page-testimonials">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">نظرات بیماران</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openTestimModal()">+ افزودن نظر</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>نام</th><th>متن</th><th>امتیاز</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="testimonialsTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── BLOG POSTS ── -->
|
||||
<div class="page" id="page-blogposts">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">مقالات وبلاگ</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openPostEditor()">+ مقاله جدید</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>عنوان</th><th>دسته</th><th>کلیدواژه</th><th>بازدید</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="postsTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CATEGORIES ── -->
|
||||
<div class="page" id="page-categories">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">دستهبندیها</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openCatModal()">+ دسته جدید</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>نام</th><th>اسلاگ</th><th>مقالات</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="categoriesTable"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SEO ── -->
|
||||
<div class="page" id="page-seo">
|
||||
<div class="stat-grid" style="grid-template-columns:repeat(3,1fr)">
|
||||
<div class="stat-card"><div class="label">مقالات منتشرشده</div><div class="value" id="seo-posts">-</div></div>
|
||||
<div class="stat-card"><div class="label">کل بازدید</div><div class="value" id="seo-views">-</div></div>
|
||||
<div class="stat-card"><div class="label">مقالات بدون متا</div><div class="value" id="seo-nometa" style="color:var(--danger)">-</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">پربازدیدترین مقالات</div></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>عنوان</th><th>بازدید</th></tr></thead><tbody id="seoTopPosts"></tbody></table></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">راهنمای بهبود SEO</div></div>
|
||||
<div class="card-body" style="font-size:.88rem;line-height:2;color:var(--mid)">
|
||||
<p>✅ هر مقاله باید <strong>MetaTitle</strong> زیر ۷۰ کاراکتر داشته باشد</p>
|
||||
<p>✅ <strong>MetaDescription</strong> بین ۱۵۰-۱۶۰ کاراکتر</p>
|
||||
<p>✅ <strong>کلیدواژه اصلی</strong> در عنوان، اول پاراگراف و URL وجود داشته باشد</p>
|
||||
<p>✅ هر مقاله حداقل <strong>۵۰۰ کلمه</strong> داشته باشد</p>
|
||||
<p>✅ از <strong>H2 و H3</strong> در ساختار مقاله استفاده کنید</p>
|
||||
<p>✅ لینکدهی داخلی بین مقالات انجام دهید</p>
|
||||
<p>✅ تصاویر دارای <strong>alt text</strong> فارسی باشند</p>
|
||||
<p>🔗 <a href="../sitemap.xml" target="_blank" style="color:var(--gold)">مشاهده Sitemap</a> | <a href="../robots.txt" target="_blank" style="color:var(--gold)">مشاهده Robots.txt</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /page-content -->
|
||||
</div><!-- /main -->
|
||||
|
||||
<!-- ══ MODALS ══ -->
|
||||
|
||||
<!-- Service Modal -->
|
||||
<div class="modal-overlay hidden" id="svcModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title" id="svcModalTitle">افزودن خدمت</div><button class="modal-close" onclick="closeModal('svcModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="svc-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>عنوان</label><input id="svc-title"/></div>
|
||||
<div class="form-group"><label>توضیح</label><textarea id="svc-desc" rows="3"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>ترتیب</label><input type="number" id="svc-order" value="1"/></div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="svc-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveSvc()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('svcModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Modal -->
|
||||
<div class="modal-overlay hidden" id="galleryModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title" id="galleryModalTitle">افزودن تصویر</div><button class="modal-close" onclick="closeModal('galleryModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="gal-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>آدرس تصویر اصلی</label><input id="gal-img" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>آدرس تصویر قبل</label><input id="gal-before" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-group"><label>آدرس تصویر بعد</label><input id="gal-after" dir="ltr" placeholder="https://..."/></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>دستهبندی</label><input id="gal-cat" placeholder="بوتاکس، لیزر..."/></div>
|
||||
<div class="form-group"><label>ترتیب</label><input type="number" id="gal-order" value="1"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>توضیح</label><input id="gal-caption"/></div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="gal-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveGallery()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('galleryModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testimonial Modal -->
|
||||
<div class="modal-overlay hidden" id="testimModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><div class="modal-title">افزودن / ویرایش نظر</div><button class="modal-close" onclick="closeModal('testimModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="testim-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نام</label><input id="testim-name"/></div>
|
||||
<div class="form-group"><label>ایموجی</label><input id="testim-emoji" value="👩"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>متن نظر</label><textarea id="testim-text" rows="3"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>امتیاز (۱-۵)</label><input type="number" id="testim-rating" value="5" min="1" max="5"/></div>
|
||||
<div class="form-group"><label>تاریخ</label><input id="testim-date" placeholder="اردیبهشت ۱۴۰۳"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>وضعیت</label><select id="testim-active"><option value="true">فعال</option><option value="false">غیرفعال</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveTestim()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('testimModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Modal -->
|
||||
<div class="modal-overlay hidden" id="catModal">
|
||||
<div class="modal" style="max-width:480px">
|
||||
<div class="modal-header"><div class="modal-title" id="catModalTitle">دسته جدید</div><button class="modal-close" onclick="closeModal('catModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="cat-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>نام</label><input id="cat-name"/></div>
|
||||
<div class="form-group"><label>اسلاگ (خودکار)</label><input id="cat-slug" dir="ltr" placeholder="auto-generated"/></div>
|
||||
<div class="form-group"><label>توضیح</label><textarea id="cat-desc" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveCat()">ذخیره</button><button class="btn btn-secondary" onclick="closeModal('catModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog Post Editor Modal -->
|
||||
<div class="modal-overlay hidden" id="postModal">
|
||||
<div class="modal" style="max-width:900px">
|
||||
<div class="modal-header"><div class="modal-title" id="postModalTitle">مقاله جدید</div><button class="modal-close" onclick="closeModal('postModal')">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="post-id"/>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>عنوان مقاله</label><input id="post-title" oninput="autoSlug()"/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>اسلاگ (URL)</label><input id="post-slug" dir="ltr"/></div>
|
||||
<div class="form-group"><label>دستهبندی</label><select id="post-category"><option value="">انتخاب دسته...</option></select></div>
|
||||
</div>
|
||||
<div class="form-group"><label>خلاصه (Excerpt)</label><textarea id="post-excerpt" rows="2"></textarea></div>
|
||||
<div class="form-group">
|
||||
<label>محتوای مقاله</label>
|
||||
<div class="editor-toolbar">
|
||||
<button onclick="fmt('bold')"><b>B</b></button>
|
||||
<button onclick="fmt('italic')"><i>I</i></button>
|
||||
<button onclick="fmtBlock('h2')">H2</button>
|
||||
<button onclick="fmtBlock('h3')">H3</button>
|
||||
<button onclick="fmtBlock('p')">P</button>
|
||||
<button onclick="fmt('insertUnorderedList')">• لیست</button>
|
||||
<button onclick="fmt('insertOrderedList')">۱. لیست</button>
|
||||
<button onclick="insLink()">🔗 لینک</button>
|
||||
</div>
|
||||
<div class="editor-content" id="post-content" contenteditable="true" dir="rtl"></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:1rem;margin-top:.5rem">
|
||||
<p style="font-size:.82rem;font-weight:600;color:var(--gold);margin-bottom:1rem">تنظیمات SEO</p>
|
||||
<div class="form-group"><label>کلیدواژه اصلی (Focus Keyword)</label><input id="post-focus" oninput="checkSeo()"/><div class="form-hint">کلیدواژهای که میخواهید برای آن رتبه بگیرید</div></div>
|
||||
<div id="seoFeedback" style="margin:.5rem 0"></div>
|
||||
<div class="form-group"><label>Meta Title <span style="font-weight:400;color:var(--light)" id="mtLen">(0/70)</span></label><input id="post-metatitle" oninput="updateLen('post-metatitle','mtLen',70)"/></div>
|
||||
<div class="form-group"><label>Meta Description <span style="font-weight:400;color:var(--light)" id="mdLen">(0/160)</span></label><textarea id="post-metadesc" rows="2" oninput="updateLen('post-metadesc','mdLen',160)"></textarea></div>
|
||||
<div class="form-group"><label>کلیدواژهها (با کاما جدا کنید)</label><input id="post-keywords" placeholder="بوتاکس, جوانسازی, پوست..."/></div>
|
||||
<div class="form-group"><label>تصویر شاخص</label><input id="post-image" dir="ltr" placeholder="https://..."/></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نوع Schema</label><select id="post-schematype"><option value="MedicalWebPage">MedicalWebPage</option><option value="Article">Article</option><option value="FAQPage">FAQPage</option></select></div>
|
||||
<div class="form-group"><label>وضعیت انتشار</label><select id="post-published"><option value="false">پیشنویس</option><option value="true">منتشر</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="savePost()">ذخیره مقاله</button><button class="btn btn-secondary" onclick="closeModal('postModal')">انصراف</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script>
|
||||
const API = 'http://localhost:5000';
|
||||
let token = localStorage.getItem('dr_token') || '';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
async function doLogin() {
|
||||
const u = document.getElementById('loginUser').value;
|
||||
const p = document.getElementById('loginPass').value;
|
||||
const r = await fetch(`${API}/api/auth/login`, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({username:u, password:p})
|
||||
});
|
||||
if (!r.ok) { document.getElementById('loginErr').style.display='block'; return; }
|
||||
const d = await r.json();
|
||||
token = d.token;
|
||||
localStorage.setItem('dr_token', token);
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
init();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('dr_token');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// ── API helper ────────────────────────────────────────────────────────────────
|
||||
async function api(path, opts={}) {
|
||||
const h = {'Content-Type':'application/json'};
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
const r = await fetch(`${API}${path}`, {...opts, headers:{...h, ...(opts.headers||{})}});
|
||||
if (r.status === 401) { logout(); return null; }
|
||||
if (r.status === 204) return null;
|
||||
if (!r.ok) { toast('خطا در سرور: ' + r.status, 'error'); return null; }
|
||||
return r.json().catch(()=>null);
|
||||
}
|
||||
|
||||
function toast(msg, type='success') {
|
||||
const c = document.getElementById('toastContainer');
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast ${type}`;
|
||||
el.textContent = msg;
|
||||
c.appendChild(el);
|
||||
setTimeout(()=>el.remove(), 3500);
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دستهبندیها',seo:'گزارش SEO'};
|
||||
|
||||
function showPage(name, el) {
|
||||
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
|
||||
document.getElementById('page-'+name).classList.add('active');
|
||||
el.classList.add('active');
|
||||
document.getElementById('pageTitle').textContent = pageTitles[name] || name;
|
||||
loadPage(name);
|
||||
}
|
||||
|
||||
function loadPage(name) {
|
||||
if (name==='dashboard') loadDashboard();
|
||||
else if (name==='hero') loadSection('hero');
|
||||
else if (name==='about') loadSection('about');
|
||||
else if (name==='contact') loadSection('contact');
|
||||
else if (name==='services') loadServices();
|
||||
else if (name==='gallery') loadGallery();
|
||||
else if (name==='testimonials') loadTestimonials();
|
||||
else if (name==='blogposts') loadPosts();
|
||||
else if (name==='categories') loadCategories();
|
||||
else if (name==='seo') loadSeo();
|
||||
}
|
||||
|
||||
// ── Dashboard ─────────────────────────────────────────────────────────────────
|
||||
async function loadDashboard() {
|
||||
const [seo, testims] = await Promise.all([
|
||||
api('/api/seo/stats'),
|
||||
api('/api/testimonials/all')
|
||||
]);
|
||||
if (seo) {
|
||||
document.getElementById('ds-posts').textContent = seo.total;
|
||||
document.getElementById('ds-views').textContent = seo.views;
|
||||
document.getElementById('ds-nometa').textContent = seo.noMeta;
|
||||
const tb = document.getElementById('topPostsTable');
|
||||
tb.innerHTML = seo.topPosts.map(p=>`<tr><td>${p.title}</td><td><span class="badge badge-gold">${p.viewCount}</span></td><td><a href="../post.html?slug=${p.slug}" target="_blank" class="btn btn-secondary btn-sm">مشاهده</a></td></tr>`).join('');
|
||||
}
|
||||
if (testims) document.getElementById('ds-testimonials').textContent = testims.length;
|
||||
}
|
||||
|
||||
// ── Section settings ──────────────────────────────────────────────────────────
|
||||
async function loadSection(sec) {
|
||||
const data = await api(`/api/settings/${sec}`);
|
||||
if (!data) return;
|
||||
Object.entries(data).forEach(([k,v])=>{
|
||||
const el = document.getElementById(`${sec}-${k}`);
|
||||
if (el) el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSection(sec) {
|
||||
const inputs = document.querySelectorAll(`[id^="${sec}-"]`);
|
||||
const settings = {};
|
||||
inputs.forEach(el=>{
|
||||
const key = el.id.replace(`${sec}-`,'');
|
||||
settings[key] = el.value;
|
||||
});
|
||||
await api(`/api/settings/${sec}`, {method:'PUT', body:JSON.stringify({settings})});
|
||||
toast('تغییرات با موفقیت ذخیره شد ✓');
|
||||
}
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────────────
|
||||
let svcs=[];
|
||||
async function loadServices() {
|
||||
svcs = await api('/api/services/all') || [];
|
||||
document.getElementById('servicesTable').innerHTML = svcs.map(s=>`
|
||||
<tr>
|
||||
<td>${s.order}</td>
|
||||
<td><strong>${s.title}</strong></td>
|
||||
<td style="max-width:300px;color:var(--mid)">${s.description.substring(0,60)}...</td>
|
||||
<td><span class="badge ${s.isActive?'badge-green':'badge-red'}">${s.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editSvc(${s.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteSvc(${s.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openSvcModal(id) {
|
||||
document.getElementById('svcModalTitle').textContent = 'افزودن خدمت';
|
||||
document.getElementById('svc-id').value='';
|
||||
document.getElementById('svc-title').value='';
|
||||
document.getElementById('svc-desc').value='';
|
||||
document.getElementById('svc-order').value=svcs.length+1;
|
||||
document.getElementById('svc-active').value='true';
|
||||
document.getElementById('svcModal').classList.remove('hidden');
|
||||
}
|
||||
function editSvc(id) {
|
||||
const s = svcs.find(x=>x.id===id);
|
||||
document.getElementById('svcModalTitle').textContent = 'ویرایش خدمت';
|
||||
document.getElementById('svc-id').value=s.id;
|
||||
document.getElementById('svc-title').value=s.title;
|
||||
document.getElementById('svc-desc').value=s.description;
|
||||
document.getElementById('svc-order').value=s.order;
|
||||
document.getElementById('svc-active').value=String(s.isActive);
|
||||
document.getElementById('svcModal').classList.remove('hidden');
|
||||
}
|
||||
async function saveSvc() {
|
||||
const id = document.getElementById('svc-id').value;
|
||||
const body = {
|
||||
title: document.getElementById('svc-title').value,
|
||||
description: document.getElementById('svc-desc').value,
|
||||
order: parseInt(document.getElementById('svc-order').value),
|
||||
isActive: document.getElementById('svc-active').value==='true',
|
||||
iconSvg:''
|
||||
};
|
||||
if (id) await api(`/api/services/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/services',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('svcModal');
|
||||
toast('خدمت ذخیره شد ✓');
|
||||
loadServices();
|
||||
}
|
||||
async function deleteSvc(id) {
|
||||
if (!confirm('حذف شود؟')) return;
|
||||
await api(`/api/services/${id}`,{method:'DELETE'});
|
||||
toast('حذف شد','error');
|
||||
loadServices();
|
||||
}
|
||||
|
||||
// ── Gallery ───────────────────────────────────────────────────────────────────
|
||||
let gals=[];
|
||||
async function loadGallery() {
|
||||
gals = await api('/api/gallery/all') || [];
|
||||
document.getElementById('galleryTable').innerHTML = gals.map(g=>`
|
||||
<tr>
|
||||
<td><span class="badge badge-gold">${g.category||'—'}</span></td>
|
||||
<td>${g.caption||'—'}</td>
|
||||
<td><span class="badge ${g.isActive?'badge-green':'badge-red'}">${g.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editGallery(${g.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteGallery(${g.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openGalleryModal() {
|
||||
['id','img','before','after','cat','caption'].forEach(k=>document.getElementById(`gal-${k}`).value='');
|
||||
document.getElementById('gal-order').value=gals.length+1;
|
||||
document.getElementById('gal-active').value='true';
|
||||
document.getElementById('galleryModal').classList.remove('hidden');
|
||||
}
|
||||
function editGallery(id) {
|
||||
const g=gals.find(x=>x.id===id);
|
||||
document.getElementById('gal-id').value=g.id;
|
||||
document.getElementById('gal-img').value=g.imageUrl;
|
||||
document.getElementById('gal-before').value=g.beforeImageUrl;
|
||||
document.getElementById('gal-after').value=g.afterImageUrl;
|
||||
document.getElementById('gal-cat').value=g.category;
|
||||
document.getElementById('gal-caption').value=g.caption;
|
||||
document.getElementById('gal-order').value=g.order;
|
||||
document.getElementById('gal-active').value=String(g.isActive);
|
||||
document.getElementById('galleryModal').classList.remove('hidden');
|
||||
}
|
||||
async function saveGallery() {
|
||||
const id=document.getElementById('gal-id').value;
|
||||
const body={imageUrl:document.getElementById('gal-img').value,beforeImageUrl:document.getElementById('gal-before').value,afterImageUrl:document.getElementById('gal-after').value,category:document.getElementById('gal-cat').value,caption:document.getElementById('gal-caption').value,order:parseInt(document.getElementById('gal-order').value),isActive:document.getElementById('gal-active').value==='true'};
|
||||
if(id) await api(`/api/gallery/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/gallery',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('galleryModal'); toast('ذخیره شد ✓'); loadGallery();
|
||||
}
|
||||
async function deleteGallery(id){if(!confirm('حذف؟'))return;await api(`/api/gallery/${id}`,{method:'DELETE'});toast('حذف شد','error');loadGallery();}
|
||||
|
||||
// ── Testimonials ──────────────────────────────────────────────────────────────
|
||||
let testims=[];
|
||||
async function loadTestimonials() {
|
||||
testims=await api('/api/testimonials/all')||[];
|
||||
document.getElementById('testimonialsTable').innerHTML=testims.map(t=>`
|
||||
<tr>
|
||||
<td>${t.authorEmoji} ${t.authorName}</td>
|
||||
<td style="max-width:250px">${t.text.substring(0,60)}...</td>
|
||||
<td>${'★'.repeat(t.rating)}</td>
|
||||
<td><span class="badge ${t.isActive?'badge-green':'badge-red'}">${t.isActive?'فعال':'غیرفعال'}</span></td>
|
||||
<td><button class="btn btn-secondary btn-sm" onclick="editTestim(${t.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteTestim(${t.id})">حذف</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openTestimModal(){['id','name','emoji','text','date'].forEach(k=>document.getElementById(`testim-${k}`).value='');document.getElementById('testim-emoji').value='👩';document.getElementById('testim-rating').value=5;document.getElementById('testim-active').value='true';document.getElementById('testimModal').classList.remove('hidden');}
|
||||
function editTestim(id){const t=testims.find(x=>x.id===id);document.getElementById('testim-id').value=t.id;document.getElementById('testim-name').value=t.authorName;document.getElementById('testim-emoji').value=t.authorEmoji;document.getElementById('testim-text').value=t.text;document.getElementById('testim-rating').value=t.rating;document.getElementById('testim-date').value=t.date;document.getElementById('testim-active').value=String(t.isActive);document.getElementById('testimModal').classList.remove('hidden');}
|
||||
async function saveTestim(){const id=document.getElementById('testim-id').value;const body={authorName:document.getElementById('testim-name').value,authorEmoji:document.getElementById('testim-emoji').value,text:document.getElementById('testim-text').value,rating:parseInt(document.getElementById('testim-rating').value),date:document.getElementById('testim-date').value,isActive:document.getElementById('testim-active').value==='true'};if(id)await api(`/api/testimonials/${id}`,{method:'PUT',body:JSON.stringify(body)});else await api('/api/testimonials',{method:'POST',body:JSON.stringify(body)});closeModal('testimModal');toast('ذخیره شد ✓');loadTestimonials();}
|
||||
async function deleteTestim(id){if(!confirm('حذف؟'))return;await api(`/api/testimonials/${id}`,{method:'DELETE'});toast('حذف شد','error');loadTestimonials();}
|
||||
|
||||
// ── Blog Categories ───────────────────────────────────────────────────────────
|
||||
let cats=[];
|
||||
async function loadCategories(){
|
||||
cats=await api('/api/blog/categories')||[];
|
||||
document.getElementById('categoriesTable').innerHTML=cats.map(c=>`<tr><td><strong>${c.name}</strong></td><td dir="ltr" style="color:var(--light)">${c.slug}</td><td><span class="badge badge-gold">${c.postCount}</span></td><td><button class="btn btn-secondary btn-sm" onclick="editCat(${c.id})">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteCat(${c.id})">حذف</button></td></tr>`).join('');
|
||||
}
|
||||
function openCatModal(){document.getElementById('catModalTitle').textContent='دسته جدید';['id','name','slug','desc'].forEach(k=>document.getElementById(`cat-${k}`).value='');document.getElementById('catModal').classList.remove('hidden');}
|
||||
function editCat(id){const c=cats.find(x=>x.id===id);document.getElementById('catModalTitle').textContent='ویرایش دسته';document.getElementById('cat-id').value=c.id;document.getElementById('cat-name').value=c.name;document.getElementById('cat-slug').value=c.slug;document.getElementById('cat-desc').value=c.description;document.getElementById('catModal').classList.remove('hidden');}
|
||||
async function saveCat(){const id=document.getElementById('cat-id').value;const body={name:document.getElementById('cat-name').value,slug:document.getElementById('cat-slug').value,description:document.getElementById('cat-desc').value};if(id)await api(`/api/blog/categories/${id}`,{method:'PUT',body:JSON.stringify(body)});else await api('/api/blog/categories',{method:'POST',body:JSON.stringify(body)});closeModal('catModal');toast('ذخیره شد ✓');loadCategories();}
|
||||
async function deleteCat(id){if(!confirm('حذف؟'))return;await api(`/api/blog/categories/${id}`,{method:'DELETE'});toast('حذف شد','error');loadCategories();}
|
||||
|
||||
// ── Blog Posts ────────────────────────────────────────────────────────────────
|
||||
let posts=[];
|
||||
async function loadPosts(){
|
||||
posts=await api('/api/blog/posts/admin')||[];
|
||||
document.getElementById('postsTable').innerHTML=posts.map(p=>`
|
||||
<tr>
|
||||
<td><strong>${p.title}</strong></td>
|
||||
<td><span class="badge badge-gold">${p.category?.name||'—'}</span></td>
|
||||
<td style="color:var(--gold);font-size:.8rem">${p.focusKeyword||'—'}</td>
|
||||
<td>${p.viewCount}</td>
|
||||
<td><span class="badge ${p.isPublished?'badge-green':'badge-red'}">${p.isPublished?'منتشر':'پیشنویس'}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-sm" onclick="editPost(${p.id})">ویرایش</button>
|
||||
<a href="../post.html?slug=${p.slug}" target="_blank" class="btn btn-secondary btn-sm">مشاهده</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="deletePost(${p.id})">حذف</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function openPostEditor(){
|
||||
if(!cats.length) await loadCategories();
|
||||
const catSel=document.getElementById('post-category');
|
||||
catSel.innerHTML='<option value="">انتخاب دسته...</option>'+cats.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');
|
||||
document.getElementById('postModalTitle').textContent='مقاله جدید';
|
||||
['id','title','slug','focus','metatitle','metadesc','keywords','image'].forEach(k=>document.getElementById(`post-${k}`).value='');
|
||||
document.getElementById('post-content').innerHTML='';
|
||||
document.getElementById('post-excerpt').value='';
|
||||
document.getElementById('post-published').value='false';
|
||||
document.getElementById('post-schematype').value='MedicalWebPage';
|
||||
document.getElementById('post-category').value='';
|
||||
updateLen('post-metatitle','mtLen',70);
|
||||
updateLen('post-metadesc','mdLen',160);
|
||||
document.getElementById('postModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function editPost(id){
|
||||
await openPostEditor();
|
||||
const p=await api(`/api/blog/posts/id/${id}`);
|
||||
if(!p)return;
|
||||
document.getElementById('postModalTitle').textContent='ویرایش مقاله';
|
||||
document.getElementById('post-id').value=p.id;
|
||||
document.getElementById('post-title').value=p.title;
|
||||
document.getElementById('post-slug').value=p.slug;
|
||||
document.getElementById('post-excerpt').value=p.excerpt;
|
||||
document.getElementById('post-content').innerHTML=p.content;
|
||||
document.getElementById('post-focus').value=p.focusKeyword;
|
||||
document.getElementById('post-metatitle').value=p.metaTitle;
|
||||
document.getElementById('post-metadesc').value=p.metaDescription;
|
||||
document.getElementById('post-keywords').value=p.keywords;
|
||||
document.getElementById('post-image').value=p.featuredImage;
|
||||
document.getElementById('post-published').value=String(p.isPublished);
|
||||
document.getElementById('post-schematype').value=p.articleType;
|
||||
document.getElementById('post-category').value=p.categoryId||'';
|
||||
updateLen('post-metatitle','mtLen',70);
|
||||
updateLen('post-metadesc','mdLen',160);
|
||||
checkSeo();
|
||||
}
|
||||
|
||||
async function savePost(){
|
||||
const id=document.getElementById('post-id').value;
|
||||
const catId=document.getElementById('post-category').value;
|
||||
const body={
|
||||
title:document.getElementById('post-title').value,
|
||||
slug:document.getElementById('post-slug').value,
|
||||
excerpt:document.getElementById('post-excerpt').value,
|
||||
content:document.getElementById('post-content').innerHTML,
|
||||
featuredImage:document.getElementById('post-image').value,
|
||||
author:'دکتر سوسن آلطه',
|
||||
metaTitle:document.getElementById('post-metatitle').value,
|
||||
metaDescription:document.getElementById('post-metadesc').value,
|
||||
focusKeyword:document.getElementById('post-focus').value,
|
||||
keywords:document.getElementById('post-keywords').value,
|
||||
articleType:document.getElementById('post-schematype').value,
|
||||
isPublished:document.getElementById('post-published').value==='true',
|
||||
categoryId:catId?parseInt(catId):null,
|
||||
ogImage:document.getElementById('post-image').value,
|
||||
};
|
||||
if(id) await api(`/api/blog/posts/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/blog/posts',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('postModal'); toast('مقاله ذخیره شد ✓'); loadPosts();
|
||||
}
|
||||
|
||||
async function deletePost(id){if(!confirm('حذف مقاله؟'))return;await api(`/api/blog/posts/${id}`,{method:'DELETE'});toast('مقاله حذف شد','error');loadPosts();}
|
||||
|
||||
// ── SEO ───────────────────────────────────────────────────────────────────────
|
||||
async function loadSeo(){
|
||||
const s=await api('/api/seo/stats');
|
||||
if(!s)return;
|
||||
document.getElementById('seo-posts').textContent=s.total;
|
||||
document.getElementById('seo-views').textContent=s.views;
|
||||
document.getElementById('seo-nometa').textContent=s.noMeta;
|
||||
document.getElementById('seoTopPosts').innerHTML=s.topPosts.map(p=>`<tr><td>${p.title}</td><td><span class="badge badge-gold">${p.viewCount}</span></td></tr>`).join('');
|
||||
}
|
||||
|
||||
// ── SEO helpers ───────────────────────────────────────────────────────────────
|
||||
function autoSlug(){
|
||||
const t=document.getElementById('post-title').value;
|
||||
const s=t.trim().replace(/\s+/g,'-').replace(/[^-ۿa-z0-9\-]/g,'').toLowerCase();
|
||||
document.getElementById('post-slug').value=s;
|
||||
checkSeo();
|
||||
}
|
||||
|
||||
function updateLen(inputId,labelId,max){
|
||||
const el=document.getElementById(inputId);
|
||||
const len=el.value.length;
|
||||
const label=document.getElementById(labelId);
|
||||
label.textContent=`(${len}/${max})`;
|
||||
label.style.color=len>max?'var(--danger)':(len>max*.8?'var(--gold)':'var(--light)');
|
||||
}
|
||||
|
||||
function checkSeo(){
|
||||
const kw=document.getElementById('post-focus').value.trim();
|
||||
const title=document.getElementById('post-title').value;
|
||||
const mt=document.getElementById('post-metatitle').value;
|
||||
const md=document.getElementById('post-metadesc').value;
|
||||
const fb=document.getElementById('seoFeedback');
|
||||
if(!kw){fb.innerHTML='';return;}
|
||||
const checks=[
|
||||
{ok:title.includes(kw), msg:'کلیدواژه در عنوان'},
|
||||
{ok:mt.includes(kw), msg:'کلیدواژه در Meta Title'},
|
||||
{ok:md.includes(kw), msg:'کلیدواژه در Meta Description'},
|
||||
{ok:mt.length<=70&&mt.length>0, msg:'طول Meta Title مناسب'},
|
||||
{ok:md.length<=160&&md.length>100, msg:'طول Meta Description مناسب'},
|
||||
];
|
||||
const pass=checks.filter(c=>c.ok).length;
|
||||
const cls=pass>=4?'good':pass>=2?'ok':'bad';
|
||||
const emoji=pass>=4?'✅':pass>=2?'⚠️':'❌';
|
||||
fb.innerHTML=`<div class="seo-score ${cls}">${emoji} امتیاز SEO: ${pass}/${checks.length} — ${checks.map(c=>`<span style="opacity:${c.ok?1:.4}">${c.ok?'✓':'✗'} ${c.msg}</span>`).join(' | ')}</div>`;
|
||||
}
|
||||
|
||||
// ── Editor helpers ────────────────────────────────────────────────────────────
|
||||
function fmt(cmd){document.getElementById('post-content').focus();document.execCommand(cmd);}
|
||||
function fmtBlock(tag){document.getElementById('post-content').focus();document.execCommand('formatBlock',false,tag);}
|
||||
function insLink(){const url=prompt('آدرس لینک:');if(url){document.getElementById('post-content').focus();document.execCommand('createLink',false,url);}}
|
||||
|
||||
// ── Modal helpers ─────────────────────────────────────────────────────────────
|
||||
function closeModal(id){document.getElementById(id).classList.add('hidden');}
|
||||
document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.add('hidden');}));
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
async function init(){
|
||||
if(!token){return;}
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
loadDashboard();
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
// Auto-login if token exists
|
||||
if(token) init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user