first commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
@page "/blog"
|
||||
@model DrSousan.Api.Pages.Blog.BlogIndexModel
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="مقالات تخصصی دکتر سوسن آلطه درباره زیبایی پوست، بوتاکس، فیلر، لیزر و مراقبت از پوست." />
|
||||
<link rel="canonical" href="@(Request.Scheme + "://" + Request.Host + "/blog")" />
|
||||
<style>
|
||||
/* ─── Blog Hero ─────────────────────────────────────────────── */
|
||||
.blog-hero{background:linear-gradient(135deg,var(--gold-pale) 0%,#EDE0CA 100%);padding:6rem 2rem 3rem;text-align:center}
|
||||
.blog-hero h1{font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;color:var(--dark);margin-bottom:.6rem}
|
||||
.blog-hero p{font-size:1rem;color:var(--mid);max-width:520px;margin:0 auto}
|
||||
/* ─── Search ─────────────────────────────────────────────────── */
|
||||
.search-wrap{max-width:480px;margin:1.5rem auto 0;position:relative}
|
||||
.search-wrap input{width:100%;border:1.5px solid var(--border);border-radius:50px;padding:.65rem 1.2rem .65rem 3rem;font-family:'Vazirmatn',sans-serif;font-size:.9rem;direction:rtl;outline:none;background:var(--white);transition:border-color .2s}
|
||||
.search-wrap input:focus{border-color:var(--gold)}
|
||||
.search-wrap svg{position:absolute;left:1rem;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--light)}
|
||||
/* ─── Filter ─────────────────────────────────────────────────── */
|
||||
.filter-bar{max-width:1100px;margin:2rem auto 0;padding:0 2rem;display:flex;gap:.6rem;flex-wrap:wrap}
|
||||
.filter-btn{background:transparent;border:1.5px solid var(--border);color:var(--mid);padding:.4rem 1.1rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.85rem;cursor:pointer;transition:all .2s;text-decoration:none;display:inline-block}
|
||||
.filter-btn.active,.filter-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
|
||||
/* ─── Blog Grid ──────────────────────────────────────────────── */
|
||||
.blog-grid{max-width:1100px;margin:2rem auto;padding:0 2rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem}
|
||||
@@media(max-width:900px){.blog-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@@media(max-width:600px){.blog-grid{grid-template-columns:1fr}}
|
||||
.post-card{background:var(--white);border-radius:16px;border:1px solid var(--border);overflow:hidden;transition:transform .3s,box-shadow .3s;display:flex;flex-direction:column}
|
||||
.post-card:hover{transform:translateY(-4px);box-shadow:0 12px 40px rgba(184,149,90,.15)}
|
||||
.post-card-img{aspect-ratio:16/9;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:2rem;overflow:hidden}
|
||||
.post-card-img img{width:100%;height:100%;object-fit:cover}
|
||||
.post-card-body{padding:1.3rem;flex:1;display:flex;flex-direction:column;gap:.6rem}
|
||||
.post-cat{font-size:.72rem;font-weight:600;color:var(--gold);background:var(--gold-pale);padding:.2rem .7rem;border-radius:50px;display:inline-block}
|
||||
.post-title{font-size:1rem;font-weight:600;color:var(--dark);line-height:1.5}
|
||||
.post-title:hover{color:var(--gold)}
|
||||
.post-excerpt{font-size:.85rem;color:var(--mid);line-height:1.7;flex:1}
|
||||
.post-meta{display:flex;align-items:center;justify-content:space-between;font-size:.75rem;color:var(--light);margin-top:auto;padding-top:.6rem;border-top:1px solid var(--border)}
|
||||
.read-more{color:var(--gold);font-weight:500;font-size:.82rem}
|
||||
/* ─── Empty ──────────────────────────────────────────────────── */
|
||||
.empty{text-align:center;padding:4rem 2rem;color:var(--light);grid-column:1/-1}
|
||||
/* ─── Pagination ─────────────────────────────────────────────── */
|
||||
.pagination{display:flex;gap:.5rem;justify-content:center;padding:2rem;margin-top:1rem;max-width:1100px;margin-left:auto;margin-right:auto}
|
||||
.page-btn{width:38px;height:38px;border-radius:8px;border:1.5px solid var(--border);background:transparent;font-family:'Vazirmatn',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;text-decoration:none;color:var(--dark);font-size:.9rem}
|
||||
.page-btn.active,.page-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--white)}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="blog-hero">
|
||||
<h1>وبلاگ تخصصی پوست و زیبایی</h1>
|
||||
<p>آخرین مقالات و راهنماهای تخصصی درباره مراقبت از پوست، زیبایی و درمانهای تخصصی</p>
|
||||
<div class="search-wrap">
|
||||
<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>
|
||||
<input type="text" id="searchInput" placeholder="جستجو در مقالات..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<a href="/blog" class="filter-btn @(string.IsNullOrEmpty(Model.ActiveCat) ? "active" : "")">همه</a>
|
||||
@foreach (var cat in Model.Categories)
|
||||
{
|
||||
var active = Model.ActiveCat == cat.Slug;
|
||||
var count = cat.Posts.Count(p => p.IsPublished);
|
||||
<a href="/blog?category=@cat.Slug" class="filter-btn @(active ? "active" : "")">@cat.Name (@count)</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="blog-grid" id="blogGrid">
|
||||
@if (!Model.Posts.Any())
|
||||
{
|
||||
<div class="empty"><p>مقالهای یافت نشد.</p></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<div class="post-card">
|
||||
<div class="post-card-img">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
<div class="post-card-body">
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="post-cat">@post.Category.Name</span>
|
||||
}
|
||||
<a href="/blog/@post.Slug" class="post-title">@post.Title</a>
|
||||
<p class="post-excerpt">@(post.Excerpt.Length > 120 ? post.Excerpt.Substring(0, 120) + "..." : post.Excerpt)</p>
|
||||
<div class="post-meta">
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه | 👁 @post.ViewCount</span>
|
||||
<a href="/blog/@post.Slug" class="read-more">ادامه مطلب ←</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.TotalPages > 1)
|
||||
{
|
||||
<div class="pagination">
|
||||
@if (Model.CurrentPage > 1)
|
||||
{
|
||||
<a class="page-btn" href="/blog?page=@(Model.CurrentPage - 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">‹</a>
|
||||
}
|
||||
@for (int p = 1; p <= Model.TotalPages; p++)
|
||||
{
|
||||
<a class="page-btn @(p == Model.CurrentPage ? "active" : "")"
|
||||
href="/blog?page=@p@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">@p</a>
|
||||
}
|
||||
@if (Model.CurrentPage < Model.TotalPages)
|
||||
{
|
||||
<a class="page-btn" href="/blog?page=@(Model.CurrentPage + 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">›</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages.Blog;
|
||||
|
||||
public class BlogIndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private const int PageSize = 10;
|
||||
|
||||
public BlogIndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<BlogPost> Posts { get; private set; } = new();
|
||||
public List<BlogCategory> Categories { get; private set; } = new();
|
||||
public int CurrentPage { get; private set; } = 1;
|
||||
public int TotalPages { get; private set; } = 1;
|
||||
public int TotalPosts { get; private set; } = 0;
|
||||
public string? ActiveCat { get; private set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int page = 1, string? category = null)
|
||||
{
|
||||
CurrentPage = page < 1 ? 1 : page;
|
||||
ActiveCat = category;
|
||||
|
||||
var q = _db.BlogPosts.Include(p => p.Category).Where(p => p.IsPublished);
|
||||
|
||||
if (!string.IsNullOrEmpty(category))
|
||||
q = q.Where(p => p.Category != null && p.Category.Slug == category);
|
||||
|
||||
TotalPosts = await q.CountAsync();
|
||||
TotalPages = Math.Max(1, (int)Math.Ceiling(TotalPosts / (double)PageSize));
|
||||
|
||||
if (CurrentPage > TotalPages) CurrentPage = TotalPages;
|
||||
|
||||
Posts = await q
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
Categories = await _db.BlogCategories
|
||||
.Include(c => c.Posts)
|
||||
.ToListAsync();
|
||||
|
||||
ViewData["SiteName"] = await GetSiteNameAsync();
|
||||
ViewData["Title"] = "وبلاگ | دکتر سوسن آلطه";
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<string> GetSiteNameAsync()
|
||||
{
|
||||
var s = await _db.SiteSettings
|
||||
.FirstOrDefaultAsync(x => x.Section == "hero" && x.Key == "name");
|
||||
return s?.Value ?? "دکتر سوسن آلطه";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
@page "/blog/{slug}"
|
||||
@model DrSousan.Api.Pages.Blog.PostModel
|
||||
@{
|
||||
var post = Model.Post!;
|
||||
var baseUrl = Request.Scheme + "://" + Request.Host;
|
||||
var canonicalUrl = baseUrl + "/blog/" + post.Slug;
|
||||
var ogImage = ViewData["OgImage"]?.ToString() ?? "";
|
||||
var articleType = ViewData["ArticleType"]?.ToString() ?? "MedicalWebPage";
|
||||
var pubDate = post.PublishedAt?.ToString("yyyy-MM-ddTHH:mm:ssZ") ?? "";
|
||||
var updDate = post.UpdatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="@ViewData["MetaDesc"]" />
|
||||
@if (!string.IsNullOrEmpty(ViewData["Keywords"]?.ToString())) {
|
||||
<meta name="keywords" content="@ViewData["Keywords"]" />
|
||||
}
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content="@ViewData["Title"]" />
|
||||
<meta property="og:description" content="@ViewData["MetaDesc"]" />
|
||||
<meta property="og:url" content="@canonicalUrl" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||
<meta property="og:image" content="@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)" />
|
||||
}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="@ViewData["Title"]" />
|
||||
<meta name="twitter:description" content="@ViewData["MetaDesc"]" />
|
||||
<link rel="canonical" href="@canonicalUrl" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "@articleType",
|
||||
"headline": "@post.Title.Replace("\"","\\\"") ",
|
||||
"description": "@(ViewData["MetaDesc"]?.ToString()?.Replace("\"","\\\""))",
|
||||
"author": { "@@type": "Person", "name": "@post.Author" },
|
||||
"publisher": {
|
||||
"@@type": "Organization",
|
||||
"name": "@ViewData["SiteName"]",
|
||||
"url": "@baseUrl"
|
||||
},
|
||||
"datePublished": "@pubDate",
|
||||
"dateModified": "@updDate",
|
||||
"mainEntityOfPage": "@canonicalUrl"
|
||||
@if (!string.IsNullOrEmpty(ogImage)) {
|
||||
@:,"image": "@(ogImage.StartsWith("http") ? ogImage : baseUrl + ogImage)"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ─── Post Layout ──────────────────────────────────────────────── */
|
||||
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
||||
@@media(max-width:900px){.post-layout{grid-template-columns:1fr;padding-top:5rem}}
|
||||
/* ─── Article ──────────────────────────────────────────────────── */
|
||||
.article-hero{border-radius:16px;overflow:hidden;margin-bottom:2rem;aspect-ratio:16/6;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);display:flex;align-items:center;justify-content:center;font-size:3rem}
|
||||
.article-hero img{width:100%;height:100%;object-fit:cover}
|
||||
.article-cat{display:inline-block;background:var(--gold-pale);color:var(--gold);font-size:.75rem;font-weight:600;padding:.25rem .8rem;border-radius:50px;margin-bottom:.8rem}
|
||||
.article-title{font-size:clamp(1.5rem,3vw,2rem);font-weight:700;line-height:1.4;margin-bottom:1rem}
|
||||
.article-meta{display:flex;gap:1.5rem;font-size:.8rem;color:var(--light);margin-bottom:2rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||||
.article-meta span{display:flex;align-items:center;gap:.3rem}
|
||||
/* ─── Content ──────────────────────────────────────────────────── */
|
||||
.article-content{font-size:.95rem;line-height:2;color:var(--dark)}
|
||||
.article-content h2{font-size:1.3rem;font-weight:700;margin:1.8rem 0 .8rem;color:var(--dark);padding-bottom:.4rem;border-bottom:2px solid var(--gold-pale)}
|
||||
.article-content h3{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .6rem;color:var(--dark)}
|
||||
.article-content p{margin-bottom:1rem}
|
||||
.article-content ul,.article-content ol{padding-right:1.5rem;margin-bottom:1rem}
|
||||
.article-content li{margin-bottom:.4rem}
|
||||
.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)}
|
||||
/* ─── 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)}
|
||||
/* ─── Share ─────────────────────────────────────────────────────── */
|
||||
.share-box{margin-top:2rem;padding:1.5rem;background:var(--white);border-radius:12px;border:1px solid var(--border);text-align:center}
|
||||
.share-title{font-size:.9rem;font-weight:600;margin-bottom:1rem}
|
||||
.share-btns{display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
|
||||
.share-btn{padding:.45rem 1rem;border-radius:8px;font-family:'Vazirmatn',sans-serif;font-size:.82rem;cursor:pointer;border:none;font-weight:500}
|
||||
.share-telegram{background:#2CA5E0;color:#fff}
|
||||
.share-whatsapp{background:#25D366;color:#fff}
|
||||
.share-copy{background:var(--bg);color:var(--mid);border:1px solid var(--border)}
|
||||
/* ─── CTA ──────────────────────────────────────────────────────── */
|
||||
.cta-box{margin-top:2.5rem;padding:2rem;background:linear-gradient(135deg,var(--gold-pale),#EDE0CA);border-radius:16px;text-align:center}
|
||||
.cta-box h3{font-size:1.1rem;font-weight:700;margin-bottom:.6rem}
|
||||
.cta-box p{font-size:.88rem;color:var(--mid);margin-bottom:1.2rem}
|
||||
.cta-btn{background:var(--gold);color:#fff;padding:.7rem 1.8rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.9rem;font-weight:600;border:none;cursor:pointer;display:inline-block;text-decoration:none}
|
||||
/* ─── Sidebar ──────────────────────────────────────────────────── */
|
||||
.sidebar{position:sticky;top:90px}
|
||||
.sidebar-card{background:var(--white);border-radius:14px;border:1px solid var(--border);padding:1.4rem;margin-bottom:1.5rem}
|
||||
.sidebar-title{font-size:.88rem;font-weight:700;color:var(--dark);margin-bottom:1rem;padding-bottom:.6rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post{display:flex;gap:.8rem;margin-bottom:.9rem;padding-bottom:.9rem;border-bottom:1px solid var(--border)}
|
||||
.recent-post:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}
|
||||
.recent-img{width:56px;height:56px;border-radius:8px;background:var(--gold-pale);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:1.3rem;overflow:hidden}
|
||||
.recent-img img{width:100%;height:100%;object-fit:cover;border-radius:8px}
|
||||
.recent-title{font-size:.82rem;font-weight:600;line-height:1.4;color:var(--dark);text-decoration:none}
|
||||
.recent-title:hover{color:var(--gold)}
|
||||
.recent-date{font-size:.73rem;color:var(--light);margin-top:.2rem}
|
||||
.doctor-card{text-align:center}
|
||||
.doc-avatar{width:80px;height:80px;border-radius:50%;background:var(--gold-pale);margin:0 auto .8rem;display:flex;align-items:center;justify-content:center;font-size:2rem}
|
||||
.doc-name{font-size:.95rem;font-weight:700;color:var(--dark)}
|
||||
.doc-title{font-size:.78rem;color:var(--light);margin:.2rem 0 .8rem}
|
||||
.doc-btn{background:var(--gold);color:#fff;padding:.5rem 1.2rem;border-radius:50px;font-family:'Vazirmatn',sans-serif;font-size:.82rem;border:none;cursor:pointer;width:100%;text-decoration:none;display:block;text-align:center}
|
||||
/* ─── Comments ─────────────────────────────────────────────────── */
|
||||
.comments-section{margin-top:3rem}
|
||||
.comments-section h2{font-size:1.3rem;font-weight:700;margin-bottom:1.5rem;color:var(--dark)}
|
||||
.comment-card{background:var(--gold-pale);border-radius:12px;padding:1.2rem;margin-bottom:1rem;opacity:.98}
|
||||
.comment-card.admin-reply{background:rgba(184,149,90,0.12);border-right:3px solid var(--gold)}
|
||||
.comment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||
.comment-author{font-weight:600;font-size:.92rem;color:var(--dark)}
|
||||
.comment-author.admin{color:var(--gold)}
|
||||
.comment-date{font-size:.78rem;color:var(--light)}
|
||||
.comment-body{font-size:.88rem;color:var(--mid);line-height:1.8}
|
||||
.comment-replies{margin-top:1rem;margin-right:1.5rem}
|
||||
.comment-form-wrap{margin-top:2rem;background:var(--section-bg);border-radius:16px;padding:1.75rem;border:1px solid var(--border)}
|
||||
.comment-form-wrap h3{font-size:1rem;font-weight:700;margin-bottom:1.2rem;color:var(--dark)}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.form-group{display:flex;flex-direction:column;gap:.4rem;margin-bottom:1rem}
|
||||
.form-group label{font-size:.85rem;font-weight:500;color:var(--dark)}
|
||||
.form-group input,.form-group textarea{background:var(--white);border:1.5px solid var(--border);border-radius:10px;padding:.65rem .9rem;font-family:'Vazirmatn',sans-serif;font-size:.88rem;color:var(--dark);direction:rtl;outline:none;transition:border-color .2s}
|
||||
.form-group input:focus,.form-group textarea:focus{border-color:var(--gold)}
|
||||
.form-group textarea{resize:vertical;min-height:100px}
|
||||
.form-success{display:none;background:#e6f4ea;color:#1e7e34;border-radius:8px;padding:.8rem 1rem;text-align:center;margin-bottom:1rem;font-size:.88rem}
|
||||
.form-success.show{display:block}
|
||||
@@media(max-width:600px){.form-row{grid-template-columns:1fr}}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="post-layout">
|
||||
<!-- ── Article ── -->
|
||||
<article>
|
||||
<div class="article-hero">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="article-cat">@post.Category.Name</span>
|
||||
}
|
||||
<h1 class="article-title">@post.Title</h1>
|
||||
<div class="article-meta">
|
||||
<span>✍️ @post.Author</span>
|
||||
@if (post.PublishedAt.HasValue)
|
||||
{
|
||||
<span>📅 @post.PublishedAt.Value.ToString("yyyy/MM/dd")</span>
|
||||
}
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه مطالعه</span>
|
||||
<span>👁 @post.ViewCount بازدید</span>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
@Html.Raw(post.Content)
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(post.Keywords))
|
||||
{
|
||||
<div class="article-tags">
|
||||
@foreach (var kw in post.Keywords.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
<span class="tag">@kw.Trim()</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="share-box">
|
||||
<div class="share-title">این مقاله را به اشتراک بگذارید</div>
|
||||
<div class="share-btns">
|
||||
<a class="share-btn share-telegram" href="https://t.me/share/url?url=@Uri.EscapeDataString(canonicalUrl)&text=@Uri.EscapeDataString(post.Title)" target="_blank" rel="noopener">📱 تلگرام</a>
|
||||
<a class="share-btn share-whatsapp" href="https://wa.me/?text=@Uri.EscapeDataString(post.Title + " " + canonicalUrl)" target="_blank" rel="noopener">💬 واتساپ</a>
|
||||
<button class="share-btn share-copy" onclick="navigator.clipboard.writeText('@canonicalUrl').then(()=>{this.textContent='✓ کپی شد!';setTimeout(()=>this.textContent='🔗 کپی لینک',2000)})">🔗 کپی لینک</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<h3>آماده تحول در پوستتان هستید؟</h3>
|
||||
<p>همین امروز با دکتر سوسن آلطه مشاوره رایگان دریافت کنید</p>
|
||||
<a href="/#contact" class="cta-btn">رزرو نوبت رایگان</a>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<section class="comments-section" id="comments">
|
||||
<h2>نظرات (@Model.Comments.Count)</h2>
|
||||
|
||||
@if (!Model.Comments.Any())
|
||||
{
|
||||
<p style="color:var(--light);margin-bottom:2rem;">اولین نظر را ثبت کنید!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var comment in Model.Comments)
|
||||
{
|
||||
<div class="comment-card @(comment.IsAdminReply ? "admin-reply" : "")">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author @(comment.IsAdminReply ? "admin" : "")">
|
||||
@(comment.IsAdminReply ? "👩⚕️ " : "")@comment.AuthorName
|
||||
</span>
|
||||
<span class="comment-date">@comment.CreatedAt.ToString("yyyy/MM/dd")</span>
|
||||
</div>
|
||||
<p class="comment-body">@comment.Body</p>
|
||||
@if (comment.Replies.Any())
|
||||
{
|
||||
<div class="comment-replies">
|
||||
@foreach (var reply in comment.Replies)
|
||||
{
|
||||
<div class="comment-card admin-reply">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author admin">👩⚕️ @reply.AuthorName</span>
|
||||
<span class="comment-date">@reply.CreatedAt.ToString("yyyy/MM/dd")</span>
|
||||
</div>
|
||||
<p class="comment-body">@reply.Body</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="comment-form-wrap">
|
||||
<h3>ثبت نظر</h3>
|
||||
@if (Model.CommentSent)
|
||||
{
|
||||
<div class="form-success show">✅ نظر شما ثبت شد و پس از تأیید نمایش داده میشود.</div>
|
||||
}
|
||||
<form method="post" asp-page="/Blog/Post" asp-route-slug="@post.Slug">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentAuthor">نام *</label>
|
||||
<input asp-for="CommentAuthor" placeholder="نام شما" />
|
||||
<span asp-validation-for="CommentAuthor" style="color:red;font-size:.78rem;"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentEmail">ایمیل (اختیاری)</label>
|
||||
<input asp-for="CommentEmail" placeholder="ایمیل شما" dir="ltr" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CommentBody">نظر *</label>
|
||||
<textarea asp-for="CommentBody" placeholder="نظر خود را بنویسید..."></textarea>
|
||||
<span asp-validation-for="CommentBody" style="color:red;font-size:.78rem;"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">ارسال نظر</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<!-- ── Sidebar ── -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-card doctor-card">
|
||||
<div class="doc-avatar">👩⚕️</div>
|
||||
<div class="doc-name">@post.Author</div>
|
||||
<div class="doc-title">پزشک عمومی | متخصص زیبایی پوست</div>
|
||||
<a href="/#contact" class="doc-btn">رزرو نوبت</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">دستهبندی</div>
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<a href="/blog?category=@post.Category.Slug" style="color:var(--gold);font-size:.9rem;">@post.Category.Name ←</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(post.FocusKeyword))
|
||||
{
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">کلیدواژه اصلی</div>
|
||||
<span style="background:var(--gold-pale);padding:.3rem .7rem;border-radius:12px;font-size:.82rem;color:var(--mid);">@post.FocusKeyword</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">خدمات ما</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.5rem;font-size:.85rem">
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> بوتاکس و فیلر</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> لیزر درمانی</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مزوتراپی</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> پاکسازی پوست</a>
|
||||
<a href="/#services" style="color:var(--mid);display:flex;align-items:center;gap:.4rem"><span style="color:var(--gold)">←</span> مشاوره زیبایی</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,153 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages.Blog;
|
||||
|
||||
public class PostModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public PostModel(AppDbContext db) => _db = db;
|
||||
|
||||
public BlogPost? Post { get; private set; }
|
||||
public List<CommentVm> Comments { get; private set; } = new();
|
||||
public bool CommentSent { get; private set; } = false;
|
||||
|
||||
// Comment form binding
|
||||
[BindProperty]
|
||||
[Required(ErrorMessage = "نام الزامی است.")]
|
||||
[MaxLength(100)]
|
||||
public string CommentAuthor { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
[MaxLength(200)]
|
||||
[EmailAddress]
|
||||
public string CommentEmail { get; set; } = "";
|
||||
|
||||
[BindProperty]
|
||||
[Required(ErrorMessage = "متن نظر الزامی است.")]
|
||||
public string CommentBody { get; set; } = "";
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string slug)
|
||||
{
|
||||
var post = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.FirstOrDefaultAsync(p => p.Slug == slug && p.IsPublished);
|
||||
|
||||
if (post is null) return NotFound();
|
||||
|
||||
// Increment view count
|
||||
post.ViewCount++;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Post = post;
|
||||
await LoadCommentsAsync(post.Id);
|
||||
await SetViewDataAsync(post);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string slug)
|
||||
{
|
||||
var post = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.FirstOrDefaultAsync(p => p.Slug == slug && p.IsPublished);
|
||||
|
||||
if (post is null) return NotFound();
|
||||
|
||||
Post = post;
|
||||
await LoadCommentsAsync(post.Id);
|
||||
await SetViewDataAsync(post);
|
||||
|
||||
if (!ModelState.IsValid) return Page();
|
||||
|
||||
// Sanitise
|
||||
var body = System.Text.RegularExpressions.Regex.Replace(CommentBody ?? "", "<[^>]*>", "").Trim();
|
||||
var authorName = System.Text.RegularExpressions.Regex.Replace(CommentAuthor ?? "", "<[^>]*>", "").Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body) || string.IsNullOrWhiteSpace(authorName))
|
||||
{
|
||||
ModelState.AddModelError("", "نام و متن نظر الزامی است.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
_db.Comments.Add(new Comment
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
AuthorName = authorName,
|
||||
AuthorEmail = CommentEmail ?? "",
|
||||
Body = body,
|
||||
IsApproved = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
CommentSent = true;
|
||||
// Clear form
|
||||
CommentAuthor = "";
|
||||
CommentEmail = "";
|
||||
CommentBody = "";
|
||||
ModelState.Clear();
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task LoadCommentsAsync(int postId)
|
||||
{
|
||||
var raw = await _db.Comments
|
||||
.Where(c => c.BlogPostId == postId && c.IsApproved && c.ParentId == null)
|
||||
.OrderBy(c => c.CreatedAt)
|
||||
.Select(c => new
|
||||
{
|
||||
c.Id, c.AuthorName, c.Body, c.CreatedAt, c.IsAdminReply,
|
||||
Replies = _db.Comments
|
||||
.Where(r => r.ParentId == c.Id && r.IsApproved)
|
||||
.OrderBy(r => r.CreatedAt)
|
||||
.Select(r => new { r.Id, r.AuthorName, r.Body, r.CreatedAt, r.IsAdminReply })
|
||||
.ToList()
|
||||
}).ToListAsync();
|
||||
|
||||
Comments = raw.Select(c => new CommentVm
|
||||
{
|
||||
Id = c.Id, AuthorName = c.AuthorName, Body = c.Body,
|
||||
CreatedAt = c.CreatedAt, IsAdminReply = c.IsAdminReply,
|
||||
Replies = c.Replies.Select(r => new CommentVm
|
||||
{
|
||||
Id = r.Id, AuthorName = r.AuthorName, Body = r.Body,
|
||||
CreatedAt = r.CreatedAt, IsAdminReply = r.IsAdminReply
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task SetViewDataAsync(BlogPost post)
|
||||
{
|
||||
var metaTitle = string.IsNullOrEmpty(post.MetaTitle) ? post.Title : post.MetaTitle;
|
||||
var metaDesc = string.IsNullOrEmpty(post.MetaDescription) ? post.Excerpt : post.MetaDescription;
|
||||
|
||||
// Avoid duplicating the site name if MetaTitle already contains it
|
||||
var suffix = " | دکتر سوسن آلطه";
|
||||
ViewData["Title"] = metaTitle.Contains("دکتر سوسن") ? metaTitle : metaTitle + suffix;
|
||||
ViewData["MetaDesc"] = metaDesc;
|
||||
ViewData["Keywords"] = post.Keywords;
|
||||
ViewData["OgImage"] = string.IsNullOrEmpty(post.OgImage) ? post.FeaturedImage : post.OgImage;
|
||||
ViewData["ArticleType"] = post.ArticleType;
|
||||
ViewData["Slug"] = post.Slug;
|
||||
|
||||
var s = await _db.SiteSettings
|
||||
.FirstOrDefaultAsync(x => x.Section == "hero" && x.Key == "name");
|
||||
ViewData["SiteName"] = s?.Value ?? "دکتر سوسن آلطه";
|
||||
}
|
||||
|
||||
// View model for comments
|
||||
public class CommentVm
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string AuthorName { get; set; } = "";
|
||||
public string Body { get; set; } = "";
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool IsAdminReply { get; set; }
|
||||
public List<CommentVm> Replies { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@using DrSousan.Api.Models
|
||||
@using DrSousan.Api.Data
|
||||
@namespace DrSousan.Api.Pages.Blog
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
Reference in New Issue
Block a user