feat(gallery+editor): dedicated /gallery page, homepage teaser, in-content images
Homepage gallery: - Show only 3 before/after samples as a teaser (was: all items) - Add "مشاهده گالری کامل (N نمونه)" CTA when more than 3 exist - Remove the now-pointless category tabs from the teaser New /gallery page: - Full before/after grid with category filter tabs (deduped from data) - Responsive cards with قبل/بعد labels + captions, empty state - Added to sitemap.xml (priority 0.8) Blog content editor: - New 🖼 تصویر toolbar button inserts an uploaded image at the cursor (direct upload, no forced crop) — for richer post bodies - Responsive img styling on the public post page Note: the filler-lab-soorat cover not showing is a data issue — that post has an empty featuredImage in the DB (verified); re-upload + save fixes it. The upload/save path itself is correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
.article-content strong{color:var(--dark);font-weight:600}
|
||||
.article-content a{color:var(--gold);border-bottom:1px solid var(--gold-pale)}
|
||||
.article-content blockquote{border-right:4px solid var(--gold);padding:.8rem 1.2rem;background:var(--gold-pale);border-radius:0 8px 8px 0;margin:1.2rem 0;font-style:italic;color:var(--mid)}
|
||||
.article-content img{max-width:100%;height:auto;border-radius:12px;margin:1.2rem 0;display:block}
|
||||
/* ─── Tags ─────────────────────────────────────────────────────── */
|
||||
.article-tags{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--border);display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.tag{background:var(--bg);border:1px solid var(--border);padding:.25rem .75rem;border-radius:50px;font-size:.78rem;color:var(--mid)}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
@page "/gallery"
|
||||
@model DrSousan.Api.Pages.GalleryModel
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="گالری نتایج واقعی قبل و بعد درمانهای زیبایی پوست دکتر سوسن آلطه — بوتاکس، فیلر، لیزر، مزوتراپی و پاکسازی پوست." />
|
||||
<link rel="canonical" href="@((Environment.GetEnvironmentVariable("SITE_BASE_URL")?.TrimEnd('/') ?? (Request.Scheme + "://" + Request.Host)) + "/gallery")" />
|
||||
<style>
|
||||
.gal-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
||||
.gal-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
||||
.gal-hero p{font-size:1rem;color:var(--mid);max-width:560px;margin:0 auto}
|
||||
.gal-tabs{display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap;max-width:1100px;margin:2rem auto 0;padding:0 2rem}
|
||||
.gal-tab{background:transparent;border:1.5px solid var(--border);color:var(--mid);padding:.45rem 1.2rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.85rem;cursor:pointer;transition:all .2s}
|
||||
.gal-tab.active,.gal-tab:hover{background:var(--gold);border-color:var(--gold);color:#fff}
|
||||
.gal-grid{max-width:1100px;margin:2.5rem auto;padding:0 2rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.2rem}
|
||||
@@media(max-width:900px){.gal-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@@media(max-width:600px){.gal-grid{grid-template-columns:repeat(2,1fr);gap:.8rem}}
|
||||
@@media(max-width:380px){.gal-grid{grid-template-columns:1fr}}
|
||||
.gal-item{border-radius:16px;overflow:hidden;background:var(--white);border:1px solid var(--border);display:flex;flex-direction:column}
|
||||
.gal-imgwrap{aspect-ratio:4/3;position:relative;overflow:hidden;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA)}
|
||||
.gal-imgwrap img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .4s}
|
||||
.gal-item:hover .gal-imgwrap img{transform:scale(1.05)}
|
||||
.gal-ba{display:flex;flex-direction:row;height:100%}
|
||||
.gal-ba .half{flex:1;position:relative;overflow:hidden}
|
||||
.gal-ba .divider{width:2px;background:var(--border);flex-shrink:0}
|
||||
.gal-labels{display:flex}
|
||||
.gal-labels span{flex:1;text-align:center;padding:6px 8px;font-size:.78rem;font-weight:600;color:var(--mid);border-top:1px solid var(--border)}
|
||||
.gal-labels span:first-child{border-left:1px solid var(--border)}
|
||||
.gal-caption{padding:.55rem .8rem;font-size:.8rem;color:var(--mid);text-align:center;border-top:1px solid var(--border);line-height:1.5}
|
||||
.gal-empty{text-align:center;padding:4rem 2rem;color:var(--light)}
|
||||
.gal-back{text-align:center;padding:1rem 2rem 4rem}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="gal-hero">
|
||||
<h1>گالری نتایج قبل و بعد</h1>
|
||||
<p>نمونهای از نتایج واقعی درمانهای انجامشده توسط دکتر سوسن آلطه. روی هر تصویر، قبل و بعد درمان قابل مشاهده است.</p>
|
||||
</div>
|
||||
|
||||
@if (Model.Categories.Any())
|
||||
{
|
||||
<div class="gal-tabs">
|
||||
<button class="gal-tab active" data-filter="">همه</button>
|
||||
@foreach (var cat in Model.Categories)
|
||||
{
|
||||
<button class="gal-tab" data-filter="@cat">@cat</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="gal-grid" id="galGrid">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<div class="gal-empty" style="grid-column:1/-1"><p>هنوز نمونهای ثبت نشده است.</p></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var hasBoth = !string.IsNullOrEmpty(item.BeforeImageUrl) && !string.IsNullOrEmpty(item.AfterImageUrl);
|
||||
var hasImg = !string.IsNullOrEmpty(item.ImageUrl);
|
||||
<div class="gal-item" data-cat="@item.Category">
|
||||
<div class="gal-imgwrap">
|
||||
@if (hasBoth)
|
||||
{
|
||||
<div class="gal-ba">
|
||||
<div class="half"><img src="@item.BeforeImageUrl" alt="قبل از درمان @item.Caption" loading="lazy"/></div>
|
||||
<div class="divider"></div>
|
||||
<div class="half"><img src="@item.AfterImageUrl" alt="بعد از درمان @item.Caption" loading="lazy"/></div>
|
||||
</div>
|
||||
}
|
||||
else if (hasImg)
|
||||
{
|
||||
<img src="@item.ImageUrl" alt="@item.Caption" loading="lazy"/>
|
||||
}
|
||||
</div>
|
||||
@if (hasBoth)
|
||||
{
|
||||
<div class="gal-labels"><span>قبل از درمان</span><span>بعد از درمان</span></div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(item.Caption)) { <div class="gal-caption">@item.Caption</div> }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="gal-back">
|
||||
<a href="/#contact" class="btn-primary">رزرو نوبت و مشاوره</a>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
document.querySelectorAll('.gal-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.gal-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const f = btn.dataset.filter || '';
|
||||
document.querySelectorAll('#galGrid .gal-item').forEach(item => {
|
||||
const match = f === '' || (item.dataset.cat || '') === f;
|
||||
item.style.display = match ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages;
|
||||
|
||||
public class GalleryModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public GalleryModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<GalleryItem> Items { get; private set; } = new();
|
||||
public List<string> Categories { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Items = await _db.GalleryItems
|
||||
.Where(g => g.IsActive)
|
||||
.OrderBy(g => g.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Categories = Items
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.Category))
|
||||
.Select(i => i.Category.Trim())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var siteName = (await _db.SiteSettings
|
||||
.FirstOrDefaultAsync(x => x.Section == "hero" && x.Key == "name"))?.Value
|
||||
?? "دکتر سوسن آلطه";
|
||||
ViewData["SiteName"] = siteName;
|
||||
ViewData["Title"] = $"گالری نتایج قبل و بعد | {siteName}";
|
||||
}
|
||||
}
|
||||
@@ -529,13 +529,6 @@
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">نمونهای از نتایج فوقالعاده درمانهای انجامشده توسط دکتر آلطه.</p>
|
||||
</div>
|
||||
<div class="gallery-tabs fade-in">
|
||||
<button class="tab-btn active">همه</button>
|
||||
<button class="tab-btn">بوتاکس</button>
|
||||
<button class="tab-btn">لیزر</button>
|
||||
<button class="tab-btn">مزوتراپی</button>
|
||||
<button class="tab-btn">پاکسازی</button>
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid">
|
||||
@if (Model.Gallery.Any())
|
||||
{
|
||||
@@ -592,6 +585,12 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (Model.GalleryTotal > 3)
|
||||
{
|
||||
<div class="fade-in" style="text-align:center;margin-top:2.5rem">
|
||||
<a href="/gallery" class="btn-primary">مشاهده گالری کامل (@Model.GalleryTotal نمونه)</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public class IndexModel : PageModel
|
||||
// Collections
|
||||
public List<Service> Services { get; private set; } = new();
|
||||
public List<GalleryItem> Gallery { get; private set; } = new();
|
||||
public int GalleryTotal { get; private set; } = 0;
|
||||
public List<Testimonial> Testimonials { get; private set; } = new();
|
||||
public List<BlogPost> RecentPosts { get; private set; } = new();
|
||||
public List<Faq> Faqs { get; private set; } = new();
|
||||
@@ -39,9 +40,12 @@ public class IndexModel : PageModel
|
||||
.OrderBy(s => s.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Gallery = await _db.GalleryItems
|
||||
.Where(g => g.IsActive)
|
||||
// Homepage shows only a teaser of 3; full set lives on /gallery
|
||||
var galleryQuery = _db.GalleryItems.Where(g => g.IsActive);
|
||||
GalleryTotal = await galleryQuery.CountAsync();
|
||||
Gallery = await galleryQuery
|
||||
.OrderBy(g => g.Order)
|
||||
.Take(3)
|
||||
.ToListAsync();
|
||||
|
||||
Testimonials = await _db.Testimonials
|
||||
|
||||
@@ -803,6 +803,7 @@ app.MapGet("/sitemap.xml", async (AppDbContext db, HttpContext ctx) =>
|
||||
}
|
||||
|
||||
Url(baseUrl + "/", "1.0", "weekly", DateTime.UtcNow);
|
||||
Url(baseUrl + "/gallery", "0.8", "weekly", DateTime.UtcNow);
|
||||
Url(baseUrl + "/blog", "0.9", "daily", DateTime.UtcNow);
|
||||
foreach (var p in published)
|
||||
Url($"{baseUrl}/blog/{p.Slug}", "0.8", "monthly", p.UpdatedAt);
|
||||
|
||||
@@ -1027,6 +1027,7 @@ tr:hover td{background:#FAFBFC}
|
||||
<button onclick="fmt('insertUnorderedList')">• لیست</button>
|
||||
<button onclick="fmt('insertOrderedList')">۱. لیست</button>
|
||||
<button onclick="insLink()">🔗 لینک</button>
|
||||
<button onclick="insImage()" title="درج تصویر در متن">🖼 تصویر</button>
|
||||
</div>
|
||||
<div class="editor-content" id="post-content" contenteditable="true" dir="rtl"></div>
|
||||
</div>
|
||||
@@ -2244,6 +2245,31 @@ function checkSeo(){
|
||||
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);}}
|
||||
// Insert an image into the post content at the cursor (uploads directly, no forced crop)
|
||||
function insImage(){
|
||||
const editor=document.getElementById('post-content');
|
||||
editor.focus();
|
||||
const sel=window.getSelection();
|
||||
const savedRange=sel.rangeCount?sel.getRangeAt(0):null;
|
||||
const fileInput=document.createElement('input');
|
||||
fileInput.type='file';
|
||||
fileInput.accept='image/jpeg,image/png,image/webp,image/gif';
|
||||
fileInput.onchange=async()=>{
|
||||
const file=fileInput.files[0];if(!file)return;
|
||||
toast('در حال آپلود تصویر...');
|
||||
try{
|
||||
const fd=new FormData();fd.append('file',file);
|
||||
const r=await fetch('/api/upload',{method:'POST',headers:{'Authorization':`Bearer ${token}`},body:fd});
|
||||
if(!r.ok)throw new Error(await r.text());
|
||||
const {url}=await r.json();
|
||||
editor.focus();
|
||||
if(savedRange){sel.removeAllRanges();sel.addRange(savedRange);}
|
||||
document.execCommand('insertHTML',false,`<img src="${url}" alt="" style="max-width:100%;height:auto;border-radius:12px;margin:1rem 0;display:block"/><p><br></p>`);
|
||||
toast('تصویر در متن درج شد ✓');
|
||||
}catch(e){toast('خطا در آپلود: '+e.message,'error');}
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
// ── Modal helpers ─────────────────────────────────────────────────────────────
|
||||
function closeModal(id){document.getElementById(id).classList.add('hidden');}
|
||||
|
||||
Reference in New Issue
Block a user