first commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/*.user
|
||||
**/*.db
|
||||
**/*.db-shm
|
||||
**/*.db-wal
|
||||
**/uploads
|
||||
**/node_modules
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/Dockerfile*
|
||||
**/*.md
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Data;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<SiteSetting> SiteSettings => Set<SiteSetting>();
|
||||
public DbSet<Service> Services => Set<Service>();
|
||||
public DbSet<GalleryItem> GalleryItems => Set<GalleryItem>();
|
||||
public DbSet<Testimonial> Testimonials => Set<Testimonial>();
|
||||
public DbSet<BlogCategory> BlogCategories => Set<BlogCategory>();
|
||||
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder mb)
|
||||
{
|
||||
mb.Entity<SiteSetting>()
|
||||
.HasIndex(s => new { s.Section, s.Key })
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<BlogPost>()
|
||||
.HasIndex(b => b.Slug)
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<BlogCategory>()
|
||||
.HasIndex(c => c.Slug)
|
||||
.IsUnique();
|
||||
|
||||
mb.Entity<Comment>()
|
||||
.HasOne(c => c.Parent)
|
||||
.WithMany(c => c.Replies)
|
||||
.HasForeignKey(c => c.ParentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# ── Stage 1: Build ────────────────────────────────────────────────────────────
|
||||
FROM 171.22.25.73:8087/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Restore dependencies first (layer-cache friendly)
|
||||
COPY DrSousan.Api.csproj NuGet.Config ./
|
||||
RUN ok=0; \
|
||||
for i in 1 2 3 4 5; do \
|
||||
dotnet restore DrSousan.Api.csproj --configfile NuGet.Config && ok=1 && break; \
|
||||
echo "Restore attempt $i failed, retrying in 30s..."; sleep 30; \
|
||||
done; \
|
||||
[ "$ok" = "1" ]
|
||||
|
||||
# Copy everything and publish
|
||||
COPY . .
|
||||
RUN dotnet publish DrSousan.Api.csproj \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--no-restore
|
||||
|
||||
# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
|
||||
FROM 171.22.25.73:8087/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Create directories for persistent volumes and set ownership
|
||||
# .NET 10 aspnet image ships with a built-in non-root user "app" (uid 1654)
|
||||
RUN mkdir -p /data /app/wwwroot/uploads
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# /data → SQLite DB (mount as named volume)
|
||||
# /app/wwwroot/uploads → uploaded images (mount as named volume)
|
||||
VOLUME ["/data", "/app/wwwroot/uploads"]
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
# Self-probe via the app's own runtime — the aspnet image has no curl/wget.
|
||||
HEALTHCHECK --interval=15s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD ["dotnet", "DrSousan.Api.dll", "--healthcheck"]
|
||||
|
||||
ENTRYPOINT ["dotnet", "DrSousan.Api.dll"]
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RazorLangVersion>latest</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@DrSousan.Api_HostAddress = http://localhost:5199
|
||||
|
||||
GET {{DrSousan.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DrSousan.Api.Models;
|
||||
|
||||
// ─── Site Settings (key-value store per section) ─────────────────────────────
|
||||
public class SiteSetting
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
[MaxLength(50)] public string Section { get; set; } = "";
|
||||
}
|
||||
|
||||
// ─── Service card ─────────────────────────────────────────────────────────────
|
||||
public class Service
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(200)] public string Title { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
[MaxLength(500)] public string IconSvg { get; set; } = "";
|
||||
[MaxLength(500)] public string BeforeImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string AfterImageUrl { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Gallery item ─────────────────────────────────────────────────────────────
|
||||
public class GalleryItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(500)] public string ImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string BeforeImageUrl { get; set; } = "";
|
||||
[MaxLength(500)] public string AfterImageUrl { get; set; } = "";
|
||||
[MaxLength(100)] public string Category { get; set; } = "";
|
||||
[MaxLength(300)] public string Caption { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Testimonial ──────────────────────────────────────────────────────────────
|
||||
public class Testimonial
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string AuthorName { get; set; } = "";
|
||||
[MaxLength(10)] public string AuthorEmoji { get; set; } = "👩";
|
||||
public string Text { get; set; } = "";
|
||||
public int Rating { get; set; } = 5;
|
||||
[MaxLength(50)] public string Date { get; set; } = "";
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Blog category ────────────────────────────────────────────────────────────
|
||||
public class BlogCategory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string Name { get; set; } = "";
|
||||
[MaxLength(120)] public string Slug { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public ICollection<BlogPost> Posts { get; set; } = new List<BlogPost>();
|
||||
}
|
||||
|
||||
// ─── Blog post ────────────────────────────────────────────────────────────────
|
||||
public class BlogPost
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Content
|
||||
[MaxLength(300)] public string Title { get; set; } = "";
|
||||
[MaxLength(320)] public string Slug { get; set; } = "";
|
||||
public string Excerpt { get; set; } = "";
|
||||
public string Content { get; set; } = "";
|
||||
[MaxLength(500)] public string FeaturedImage { get; set; } = "";
|
||||
[MaxLength(100)] public string Author { get; set; } = "دکتر سوسن آلطه";
|
||||
|
||||
// SEO
|
||||
[MaxLength(70)] public string MetaTitle { get; set; } = "";
|
||||
[MaxLength(160)] public string MetaDescription { get; set; } = "";
|
||||
[MaxLength(100)] public string FocusKeyword { get; set; } = "";
|
||||
public string Keywords { get; set; } = ""; // comma-separated
|
||||
[MaxLength(500)] public string OgImage { get; set; } = "";
|
||||
|
||||
// Schema.org
|
||||
[MaxLength(50)] public string ArticleType { get; set; } = "MedicalWebPage";
|
||||
|
||||
// Status
|
||||
public bool IsPublished { get; set; } = false;
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Metrics
|
||||
public int ViewCount { get; set; } = 0;
|
||||
public int ReadingTimeMinutes { get; set; } = 5;
|
||||
|
||||
// Relations
|
||||
public int? CategoryId { get; set; }
|
||||
public BlogCategory? Category { get; set; }
|
||||
}
|
||||
|
||||
// ─── Blog comment ────────────────────────────────────────────────────────────
|
||||
public class Comment
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(100)] public string AuthorName { get; set; } = "";
|
||||
[MaxLength(200)] public string AuthorEmail { get; set; } = "";
|
||||
public string Body { get; set; } = "";
|
||||
public bool IsApproved { get; set; } = false;
|
||||
public bool IsAdminReply { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
// Post relation
|
||||
public int BlogPostId { get; set; }
|
||||
public BlogPost? Post { get; set; }
|
||||
// Self-referential reply
|
||||
public int? ParentId { get; set; }
|
||||
public Comment? Parent { get; set; }
|
||||
public ICollection<Comment> Replies { get; set; } = new List<Comment>();
|
||||
}
|
||||
|
||||
// ─── FAQ ─────────────────────────────────────────────────────────────────────
|
||||
public class Faq
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Question { get; set; } = "";
|
||||
public string Answer { get; set; } = "";
|
||||
public int Order { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────────────
|
||||
public record LoginRequest(string Username, string Password);
|
||||
public record ChangePasswordRequest(string CurrentPassword, string NewPassword);
|
||||
public record SettingDto(string Key, string Value);
|
||||
public record BulkSettingsDto(Dictionary<string, string> Settings);
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org"
|
||||
value="https://api.nuget.org/v3/index.json"
|
||||
protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nexus"
|
||||
value="http://171.22.25.73:8081/repository/nuget-group/index.json"
|
||||
protocolVersion="3"
|
||||
allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -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
|
||||
@@ -0,0 +1,742 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
var h = Model.Hero;
|
||||
var a = Model.About;
|
||||
var c = Model.Contact;
|
||||
var siteName = h.GetValueOrDefault("name", "دکتر سوسن آلطه");
|
||||
var heroImg = h.GetValueOrDefault("image", "");
|
||||
var badgeHidden = h.GetValueOrDefault("badge_hidden", "") == "true";
|
||||
var badgeTitle = h.TryGetValue("badge_title", out var _bt) && !string.IsNullOrWhiteSpace(_bt) ? _bt : "متخصص پوست و زیبایی";
|
||||
var badgeSub = h.TryGetValue("badge_subtitle", out var _bs) && !string.IsNullOrWhiteSpace(_bs) ? _bs : "فارغالتحصیل دانشگاه ایران";
|
||||
var aboutImg = a.GetValueOrDefault("image", "");
|
||||
var yearsExp = a.GetValueOrDefault("years_exp","۱۰+");
|
||||
var ig = c.GetValueOrDefault("instagram","");
|
||||
var wa = c.GetValueOrDefault("whatsapp","");
|
||||
var tg = c.GetValueOrDefault("telegram","");
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<title>@ViewData["Title"]</title>
|
||||
<meta name="description" content="@h.GetValueOrDefault("subtitle","")" />
|
||||
<meta name="keywords" content="دکتر پوست تهران,بوتاکس,لیزر موهای زائد,فیلر,مزوتراپی,زیبایی پوست" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="@ViewData["Title"]" />
|
||||
<meta property="og:description" content="@h.GetValueOrDefault("subtitle","")" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
<link rel="canonical" href="@(Request.Scheme + "://" + Request.Host + "/")" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":["MedicalBusiness","LocalBusiness"],
|
||||
"name":"@siteName",
|
||||
"description":"@h.GetValueOrDefault("subtitle","")",
|
||||
"url":"@(Request.Scheme + "://" + Request.Host)",
|
||||
"telephone":"@c.GetValueOrDefault("phone","")",
|
||||
"address":{"@@type":"PostalAddress","addressLocality":"تهران","addressCountry":"IR","streetAddress":"@c.GetValueOrDefault("address","")"},
|
||||
"openingHours":"@c.GetValueOrDefault("hours","")",
|
||||
"medicalSpecialty":"Dermatology"
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* ─── Hero ─────────────────────────────────────────────────── */
|
||||
#hero { min-height:100svh; display:flex; align-items:center; padding:100px 0 3rem; position:relative; overflow:hidden; }
|
||||
#hero::before { content:''; position:absolute; top:-120px; left:-120px; width:600px; height:600px; background:radial-gradient(circle, rgba(184,149,90,0.10) 0%, transparent 70%); pointer-events:none; }
|
||||
#hero::after { content:''; position:absolute; bottom:-80px; right:-80px; width:500px; height:500px; background:radial-gradient(circle, rgba(184,149,90,0.07) 0%, transparent 70%); pointer-events:none; }
|
||||
.hero-inner { max-width:1200px; margin:0 auto; padding:0 2rem; display:grid; grid-template-columns:1fr 1fr; gap:4rem; align-items:center; }
|
||||
.hero-text { animation: fadeUp 0.9s ease both; }
|
||||
.hero-tag { display:inline-block; background:var(--gold-pale); color:var(--gold); font-size:0.78rem; font-weight:500; padding:0.35rem 1rem; border-radius:50px; margin-bottom:1.4rem; letter-spacing:0.05em; }
|
||||
.hero-name { font-size:clamp(2.2rem,4vw,3.2rem); font-weight:700; line-height:1.3; color:var(--dark); margin-bottom:0.6rem; }
|
||||
.hero-name span { color:var(--gold); }
|
||||
.hero-subtitle { font-size:1.05rem; color:var(--mid); font-weight:400; margin-bottom:1.8rem; line-height:1.9; }
|
||||
.hero-stats { display:flex; gap:2.5rem; margin-bottom:2.5rem; }
|
||||
.stat-item { text-align:center; }
|
||||
.stat-number { font-size:2rem; font-weight:700; color:var(--gold); display:block; line-height:1; }
|
||||
.stat-label { font-size:0.78rem; color:var(--light); margin-top:0.3rem; }
|
||||
.hero-actions { display:flex; gap:1rem; flex-wrap:wrap; }
|
||||
.hero-image { position:relative; animation:fadeUp 0.9s 0.2s ease both; }
|
||||
.hero-image-frame { width:100%; aspect-ratio:4/5; border-radius:30% 70% 70% 30% / 30% 30% 70% 70%; background:linear-gradient(145deg, var(--gold-pale) 0%, #EFE3CC 100%); position:relative; overflow:hidden; box-shadow:0 30px 80px rgba(184,149,90,0.2); }
|
||||
.hero-image-frame img { width:100%; height:100%; object-fit:cover; }
|
||||
.hero-image-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1rem; color:var(--gold); }
|
||||
.hero-image-placeholder svg { width:80px; height:80px; opacity:0.5; }
|
||||
.hero-image-placeholder p { font-size:0.85rem; opacity:0.6; }
|
||||
.hero-badge { position:absolute; bottom:2rem; left:-2rem; background:var(--white); border-radius:16px; padding:1rem 1.4rem; box-shadow:0 10px 40px rgba(0,0,0,0.1); display:flex; align-items:center; gap:0.8rem; animation:float 3s ease-in-out infinite; }
|
||||
.badge-icon { width:42px; height:42px; background:var(--gold-pale); border-radius:50%; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.badge-icon svg { width:20px; height:20px; color:var(--gold); }
|
||||
.badge-text { font-size:0.82rem; line-height:1.4; }
|
||||
.badge-text strong { color:var(--gold); display:block; font-size:0.9rem; }
|
||||
/* ─── About ─────────────────────────────────────────────────── */
|
||||
#about { background:var(--white); }
|
||||
.about-grid { display:grid; grid-template-columns:1fr 1fr; gap:5rem; align-items:center; }
|
||||
.about-image-wrap { position:relative; }
|
||||
.about-img-box { width:100%; aspect-ratio:3/4; border-radius:20px; background:linear-gradient(160deg, var(--gold-pale) 0%, #EDE0CA 100%); overflow:hidden; position:relative; }
|
||||
.about-img-box img { width:100%; height:100%; object-fit:cover; }
|
||||
.about-img-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1rem; color:var(--gold); opacity:0.5; }
|
||||
.about-img-placeholder svg { width:60px; height:60px; }
|
||||
.about-accent-box { position:absolute; bottom:-2rem; right:-2rem; background:var(--gold); color:var(--white); border-radius:16px; padding:1.5rem 2rem; text-align:center; }
|
||||
.about-accent-box .big { font-size:2.2rem; font-weight:700; display:block; }
|
||||
.about-accent-box .small { font-size:0.82rem; opacity:0.85; }
|
||||
.credential-list { list-style:none; display:flex; flex-direction:column; gap:1rem; margin-top:2rem; }
|
||||
.credential-list li { display:flex; align-items:flex-start; gap:0.85rem; font-size:0.95rem; color:var(--mid); line-height:1.6; }
|
||||
.cred-icon { width:28px; height:28px; border-radius:50%; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:0.1rem; }
|
||||
.cred-icon svg { width:13px; height:13px; color:var(--gold); }
|
||||
/* ─── Services ─────────────────────────────────────────────── */
|
||||
#services { background:var(--section-bg); }
|
||||
.services-header { text-align:center; margin-bottom:3.5rem; }
|
||||
.services-header .divider { margin:1.2rem auto 0; }
|
||||
.services-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.service-card { background:var(--white); border-radius:20px; padding:2rem; border:1px solid var(--border); transition:transform 0.3s, box-shadow 0.3s, border-color 0.3s; position:relative; overflow:hidden; }
|
||||
.service-card::before { content:''; position:absolute; top:0; right:0; width:80px; height:80px; background:radial-gradient(circle at top right, rgba(184,149,90,0.08), transparent 70%); }
|
||||
.service-card:hover { transform:translateY(-6px); box-shadow:0 20px 50px rgba(184,149,90,0.15); border-color:var(--gold-pale); }
|
||||
.service-icon { width:56px; height:56px; background:var(--gold-pale); border-radius:16px; display:flex; align-items:center; justify-content:center; margin-bottom:1.3rem; }
|
||||
.service-icon svg { width:26px; height:26px; color:var(--gold); }
|
||||
.service-title { font-size:1.05rem; font-weight:600; color:var(--dark); margin-bottom:0.6rem; }
|
||||
.service-desc { font-size:0.88rem; color:var(--mid); line-height:1.8; }
|
||||
/* ─── Service Before/After ────────────────────────────────── */
|
||||
.svc-ba { margin-top:1.2rem; border-radius:12px; overflow:hidden; position:relative; aspect-ratio:4/3; cursor:pointer; user-select:none; }
|
||||
.svc-ba img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transition:opacity 0.45s ease; }
|
||||
.svc-ba .ba-after { opacity:0; }
|
||||
.svc-ba.show-after .ba-before { opacity:0; }
|
||||
.svc-ba.show-after .ba-after { opacity:1; }
|
||||
.svc-ba-btns { position:absolute; bottom:0; left:0; right:0; display:flex; z-index:2; }
|
||||
.svc-ba-btn { flex:1; padding:0.45rem 0; text-align:center; font-size:0.78rem; font-weight:700; font-family:'Vazirmatn',sans-serif; border:none; cursor:pointer; transition:all 0.25s; background:rgba(0,0,0,0.42); color:rgba(255,255,255,0.75); letter-spacing:0.03em; }
|
||||
.svc-ba-btn.active { background:var(--gold); color:#fff; }
|
||||
.svc-ba-only { margin-top:1.2rem; border-radius:12px; overflow:hidden; position:relative; aspect-ratio:4/3; }
|
||||
.svc-ba-only img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||
.svc-ba-only .ba-solo-label { position:absolute; bottom:0; left:0; right:0; padding:0.4rem; text-align:center; font-size:0.78rem; font-weight:700; font-family:'Vazirmatn',sans-serif; background:var(--gold); color:#fff; }
|
||||
/* ─── Gallery ──────────────────────────────────────────────── */
|
||||
#gallery { background:var(--white); }
|
||||
.gallery-header { margin-bottom:3rem; }
|
||||
.gallery-tabs { display:flex; gap:0.6rem; margin-bottom:2.5rem; flex-wrap:wrap; }
|
||||
.tab-btn { background:transparent; border:1.5px solid var(--border); color:var(--mid); padding:0.45rem 1.2rem; border-radius:50px; font-family:'Vazirmatn',sans-serif; font-size:0.85rem; cursor:pointer; transition:all 0.25s; }
|
||||
.tab-btn.active, .tab-btn:hover { background:var(--gold); border-color:var(--gold); color:var(--white); }
|
||||
.gallery-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.2rem; }
|
||||
.gallery-item { border-radius:16px; overflow:hidden; aspect-ratio:4/3; background:linear-gradient(135deg, var(--gold-pale), #EDE0CA); position:relative; cursor:pointer; }
|
||||
.gallery-item img { width:100%; height:100%; object-fit:cover; transition:transform 0.4s; }
|
||||
.gallery-item:hover img { transform:scale(1.05); }
|
||||
.gallery-placeholder { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0.6rem; color:var(--gold); opacity:0.45; }
|
||||
.gallery-placeholder svg { width:36px; height:36px; }
|
||||
.gallery-placeholder p { font-size:0.75rem; }
|
||||
.gallery-item-overlay { position:absolute; inset:0; background:rgba(184,149,90,0); display:flex; align-items:center; justify-content:center; transition:background 0.3s; }
|
||||
.gallery-item:hover .gallery-item-overlay { background:rgba(184,149,90,0.15); }
|
||||
/* ─── Testimonials ─────────────────────────────────────────── */
|
||||
#testimonials { background:var(--section-bg); }
|
||||
.testimonials-header { text-align:center; margin-bottom:3rem; }
|
||||
.testimonials-header .divider { margin:1rem auto 0; }
|
||||
.testimonials-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.testimonial-card { background:var(--white); border-radius:20px; padding:2rem; border:1px solid var(--border); position:relative; }
|
||||
.testimonial-card::before { content:'"'; position:absolute; top:1rem; left:1.5rem; font-size:4rem; color:var(--gold-pale); line-height:1; font-family:Georgia,serif; }
|
||||
.testimonial-stars { display:flex; gap:3px; margin-bottom:1rem; }
|
||||
.star { color:var(--gold); font-size:1rem; }
|
||||
.testimonial-text { font-size:0.9rem; color:var(--mid); line-height:1.9; margin-bottom:1.5rem; }
|
||||
.testimonial-author { display:flex; align-items:center; gap:0.8rem; }
|
||||
.author-avatar { width:42px; height:42px; border-radius:50%; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; font-size:1.1rem; flex-shrink:0; }
|
||||
.author-name { font-size:0.88rem; font-weight:600; color:var(--dark); }
|
||||
.author-date { font-size:0.78rem; color:var(--light); }
|
||||
/* ─── Blog Preview ─────────────────────────────────────────── */
|
||||
#blog-preview { background:var(--white); }
|
||||
.blog-preview-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.5rem; }
|
||||
.post-card-prev { background:var(--bg); border-radius:16px; border:1px solid var(--border); overflow:hidden; transition:transform 0.3s,box-shadow 0.3s; display:flex; flex-direction:column; }
|
||||
.post-card-prev:hover { transform:translateY(-4px); box-shadow:0 12px 40px rgba(184,149,90,0.15); }
|
||||
.post-card-prev-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-prev-img img { width:100%; height:100%; object-fit:cover; }
|
||||
.post-card-prev-body { padding:1.3rem; flex:1; display:flex; flex-direction:column; gap:0.6rem; }
|
||||
.post-cat-label { font-size:0.72rem; font-weight:600; color:var(--gold); background:var(--gold-pale); padding:0.2rem 0.7rem; border-radius:50px; display:inline-block; }
|
||||
.post-card-prev-title { font-size:1rem; font-weight:600; color:var(--dark); line-height:1.5; }
|
||||
.post-card-prev-title:hover { color:var(--gold); }
|
||||
.post-card-prev-excerpt { font-size:0.85rem; color:var(--mid); line-height:1.7; flex:1; }
|
||||
.post-card-prev-meta { display:flex; align-items:center; justify-content:space-between; font-size:0.75rem; color:var(--light); margin-top:auto; padding-top:0.6rem; border-top:1px solid var(--border); }
|
||||
.read-more-link { color:var(--gold); font-weight:500; font-size:0.82rem; }
|
||||
.blog-preview-more { text-align:center; margin-top:2.5rem; }
|
||||
/* ─── FAQ ──────────────────────────────────────────────────── */
|
||||
#faq { background:var(--section-bg); }
|
||||
.faq-list { max-width:780px; margin:0 auto; display:flex; flex-direction:column; gap:0.8rem; }
|
||||
.faq-item { background:var(--white); border-radius:12px; border:1px solid var(--border); overflow:hidden; }
|
||||
.faq-item summary { padding:1.25rem 1.5rem; cursor:pointer; font-weight:600; list-style:none; display:flex; justify-content:space-between; align-items:center; color:var(--dark); font-size:0.95rem; }
|
||||
.faq-item summary::-webkit-details-marker { display:none; }
|
||||
.faq-item summary::after { content:"+"; color:var(--gold); font-size:1.1rem; flex-shrink:0; margin-right:1rem; }
|
||||
.faq-item[open] summary::after { content:"-"; }
|
||||
.faq-answer { padding:0 1.5rem 1.25rem; color:var(--mid); font-size:0.92rem; line-height:1.85; border-top:1px solid var(--border); }
|
||||
/* ─── Contact ──────────────────────────────────────────────── */
|
||||
#contact { background:var(--white); }
|
||||
.contact-grid { display:grid; grid-template-columns:1fr 1.4fr; gap:4rem; align-items:start; }
|
||||
.contact-info-list { display:flex; flex-direction:column; gap:1.5rem; margin-top:2rem; }
|
||||
.contact-info-item { display:flex; align-items:flex-start; gap:1rem; }
|
||||
.info-icon { width:48px; height:48px; background:var(--gold-pale); border-radius:14px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.info-icon svg { width:22px; height:22px; color:var(--gold); }
|
||||
.info-text strong { display:block; font-size:0.9rem; color:var(--dark); margin-bottom:0.2rem; }
|
||||
.info-text p { font-size:0.88rem; color:var(--mid); line-height:1.6; }
|
||||
.social-links { display:flex; gap:0.8rem; margin-top:2.5rem; }
|
||||
.social-link { width:44px; height:44px; border-radius:12px; background:var(--gold-pale); display:flex; align-items:center; justify-content:center; color:var(--gold); text-decoration:none; transition:background 0.25s, transform 0.2s; }
|
||||
.social-link:hover { background:var(--gold); color:var(--white); transform:translateY(-3px); }
|
||||
.social-link svg { width:20px; height:20px; }
|
||||
.contact-form { background:var(--section-bg); border-radius:24px; padding:2.5rem; border:1px solid var(--border); }
|
||||
.form-title { font-size:1.2rem; font-weight:600; color:var(--dark); margin-bottom:0.5rem; }
|
||||
.form-sub { font-size:0.88rem; color:var(--mid); margin-bottom:2rem; }
|
||||
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
|
||||
.form-group { display:flex; flex-direction:column; gap:0.4rem; margin-bottom:1.2rem; }
|
||||
.form-group label { font-size:0.85rem; font-weight:500; color:var(--dark); }
|
||||
.form-group input, .form-group select, .form-group textarea { background:var(--white); border:1.5px solid var(--border); border-radius:12px; padding:0.75rem 1rem; font-family:'Vazirmatn',sans-serif; font-size:0.9rem; color:var(--dark); direction:rtl; transition:border-color 0.25s, box-shadow 0.25s; outline:none; }
|
||||
.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,0.12); }
|
||||
.form-group textarea { resize:vertical; min-height:110px; }
|
||||
.form-submit { width:100%; background:var(--gold); color:var(--white); border:none; padding:0.9rem; border-radius:12px; font-family:'Vazirmatn',sans-serif; font-size:0.95rem; font-weight:600; cursor:pointer; transition:background 0.25s, transform 0.2s; }
|
||||
.form-submit:hover { background:var(--gold-light); transform:translateY(-2px); }
|
||||
/* ─── Responsive ───────────────────────────────────────────── */
|
||||
@@media (max-width:900px) {
|
||||
.hero-inner { grid-template-columns:1fr; text-align:center; gap:2.5rem; }
|
||||
.hero-text { order:1; }
|
||||
.hero-image { order:2; max-width:320px; margin:0 auto; }
|
||||
.hero-stats { justify-content:center; gap:2rem; }
|
||||
.hero-actions { justify-content:center; flex-wrap:wrap; gap:.8rem; }
|
||||
.hero-badge { left:0; right:0; margin:0 auto; width:fit-content; }
|
||||
.about-grid { grid-template-columns:1fr; gap:3rem; }
|
||||
.about-accent-box { right:0; bottom:-1rem; }
|
||||
.services-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.gallery-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.testimonials-grid { grid-template-columns:1fr; }
|
||||
.contact-grid { grid-template-columns:1fr; gap:2.5rem; }
|
||||
.blog-preview-grid { grid-template-columns:repeat(2,1fr); }
|
||||
}
|
||||
@@media (max-width:600px) {
|
||||
section { padding:3.5rem 0; }
|
||||
.container { padding:0 1.2rem; }
|
||||
.hero-inner { padding:0 1.2rem; gap:2rem; }
|
||||
.hero-image { max-width:260px; }
|
||||
.hero-stats { gap:1.5rem; }
|
||||
.hero-stats .stat-number { font-size:1.6rem; }
|
||||
.hero-badge { position:static; margin-top:1rem; width:auto; border-radius:12px; padding:.7rem 1rem; }
|
||||
.services-grid { grid-template-columns:1fr; }
|
||||
.gallery-grid { grid-template-columns:repeat(2,1fr); gap:.8rem; }
|
||||
.blog-preview-grid { grid-template-columns:1fr; }
|
||||
.form-row { grid-template-columns:1fr; }
|
||||
.about-accent-box { position:static; border-radius:12px; margin-top:1.5rem; display:inline-block; }
|
||||
.about-image-wrap { text-align:center; }
|
||||
.testimonials-grid { grid-template-columns:1fr; }
|
||||
.section-title { font-size:1.5rem; }
|
||||
.faq-item summary { font-size:.9rem; padding:1rem 1.2rem; }
|
||||
.faq-answer { padding:0 1.2rem 1rem; font-size:.88rem; }
|
||||
}
|
||||
@@media (max-width:380px) {
|
||||
.gallery-grid { grid-template-columns:1fr; }
|
||||
.hero-actions .btn-primary,
|
||||
.hero-actions .btn-secondary { width:100%; text-align:center; }
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- ══════ HERO ══════ -->
|
||||
<section id="hero">
|
||||
<div class="hero-inner">
|
||||
<div class="hero-text">
|
||||
<span class="hero-tag">@h.GetValueOrDefault("tag","پزشک عمومی و متخصص زیبایی پوست")</span>
|
||||
<h1 class="hero-name">@h.GetValueOrDefault("name_line1","دکتر") <span>@h.GetValueOrDefault("name_line2","سوسن")</span><br>@h.GetValueOrDefault("name_line3","آلطه")</h1>
|
||||
<p class="hero-subtitle">@h.GetValueOrDefault("subtitle","با بیش از یک دهه تجربه در حوزهی زیبایی و مراقبت از پوست، زیبایی واقعی شما را با علم و هنر همراه میکنیم.")</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat1_num","+۱۰")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat1_lbl","سال تجربه")</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat2_num","+۵۰۰")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat2_lbl","بیمار راضی")</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">@h.GetValueOrDefault("stat3_num","+۱۵")</span>
|
||||
<span class="stat-label">@h.GetValueOrDefault("stat3_lbl","خدمات تخصصی")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a href="#contact" class="btn-primary">@h.GetValueOrDefault("cta_primary","رزرو نوبت")</a>
|
||||
<a href="#about" class="btn-secondary">@h.GetValueOrDefault("cta_secondary","بیشتر بدانید")</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-image">
|
||||
<div class="hero-image-frame">
|
||||
@if (!string.IsNullOrEmpty(heroImg))
|
||||
{
|
||||
<img src="@heroImg" alt="@siteName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="hero-image-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<p>تصویر دکتر</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!badgeHidden)
|
||||
{
|
||||
<div class="hero-badge">
|
||||
<div class="badge-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="badge-text">
|
||||
<strong>@badgeTitle</strong>
|
||||
@badgeSub
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ ABOUT ══════ -->
|
||||
<section id="about">
|
||||
<div class="container">
|
||||
<div class="about-grid">
|
||||
<div class="about-image-wrap fade-in">
|
||||
<div class="about-img-box">
|
||||
@if (!string.IsNullOrEmpty(aboutImg))
|
||||
{
|
||||
<img src="@aboutImg" alt="@siteName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="about-img-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="about-accent-box">
|
||||
<span class="big">@yearsExp</span>
|
||||
<span class="small">سال تجربه</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fade-in">
|
||||
<span class="section-label">درباره من</span>
|
||||
<h2 class="section-title">@a.GetValueOrDefault("title","سلامت و زیبایی پوست<br>هدف اصلی من است")</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">@a.GetValueOrDefault("bio","دکتر سوسن آلطه، پزشک عمومی و متخصص زیبایی پوست با سالها تجربه در ارائه خدمات پیشرفته پوست و زیبایی.")</p>
|
||||
<ul class="credential-list">
|
||||
@foreach (var key in new[] { "cred1","cred2","cred3","cred4","cred5" })
|
||||
{
|
||||
var val = a.GetValueOrDefault(key, "");
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
<li>
|
||||
<div class="cred-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>@val</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ SERVICES ══════ -->
|
||||
<section id="services">
|
||||
<div class="container">
|
||||
<div class="services-header fade-in">
|
||||
<span class="section-label">خدمات ما</span>
|
||||
<h2 class="section-title">طیف گستردهای از خدمات<br>زیبایی و مراقبت از پوست</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="services-grid">
|
||||
@foreach (var svc in Model.Services)
|
||||
{
|
||||
var hasBefore = !string.IsNullOrEmpty(svc.BeforeImageUrl);
|
||||
var hasAfter = !string.IsNullOrEmpty(svc.AfterImageUrl);
|
||||
<div class="service-card fade-in">
|
||||
<div class="service-icon">
|
||||
@if (!string.IsNullOrEmpty(svc.IconSvg))
|
||||
{
|
||||
@Html.Raw(svc.IconSvg)
|
||||
}
|
||||
else
|
||||
{
|
||||
@Html.Raw(IndexModel.ServiceIconSvg(svc.Title))
|
||||
}
|
||||
</div>
|
||||
<div class="service-title">@svc.Title</div>
|
||||
<p class="service-desc">@svc.Description</p>
|
||||
@if (hasBefore && hasAfter)
|
||||
{
|
||||
<div class="svc-ba" onclick="toggleBA(this)" title="برای مشاهده قبل/بعد کلیک کنید">
|
||||
<img class="ba-before" src="@svc.BeforeImageUrl" alt="قبل از درمان @svc.Title" loading="lazy"/>
|
||||
<img class="ba-after" src="@svc.AfterImageUrl" alt="بعد از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="svc-ba-btns">
|
||||
<button class="svc-ba-btn active" type="button">قبل</button>
|
||||
<button class="svc-ba-btn" type="button">بعد</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (hasBefore)
|
||||
{
|
||||
<div class="svc-ba-only">
|
||||
<img src="@svc.BeforeImageUrl" alt="قبل از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="ba-solo-label">قبل از درمان</div>
|
||||
</div>
|
||||
}
|
||||
else if (hasAfter)
|
||||
{
|
||||
<div class="svc-ba-only">
|
||||
<img src="@svc.AfterImageUrl" alt="بعد از درمان @svc.Title" loading="lazy"/>
|
||||
<div class="ba-solo-label">بعد از درمان</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ GALLERY ══════ -->
|
||||
<section id="gallery">
|
||||
<div class="container">
|
||||
<div class="gallery-header fade-in">
|
||||
<span class="section-label">گالری</span>
|
||||
<h2 class="section-title">نتایج قبل و بعد</h2>
|
||||
<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">
|
||||
@if (Model.Gallery.Any())
|
||||
{
|
||||
@foreach (var item in Model.Gallery)
|
||||
{
|
||||
<div class="gallery-item fade-in">
|
||||
@if (!string.IsNullOrEmpty(item.ImageUrl))
|
||||
{
|
||||
<img src="@item.ImageUrl" alt="@item.Caption" loading="lazy" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="gallery-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<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>
|
||||
<p>تصویر قبل و بعد</p>
|
||||
</div>
|
||||
}
|
||||
<div class="gallery-item-overlay"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (int i = 0; i < 6; i++)
|
||||
{
|
||||
<div class="gallery-item fade-in">
|
||||
<div class="gallery-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<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>
|
||||
<p>تصویر قبل و بعد</p>
|
||||
</div>
|
||||
<div class="gallery-item-overlay"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ TESTIMONIALS ══════ -->
|
||||
<section id="testimonials">
|
||||
<div class="container">
|
||||
<div class="testimonials-header fade-in">
|
||||
<span class="section-label">نظرات بیماران</span>
|
||||
<h2 class="section-title">بیماران ما چه میگویند</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="testimonials-grid">
|
||||
@foreach (var t in Model.Testimonials)
|
||||
{
|
||||
<div class="testimonial-card fade-in">
|
||||
<div class="testimonial-stars">
|
||||
@for (int i = 0; i < (t.Rating > 0 ? t.Rating : 5); i++)
|
||||
{
|
||||
<span class="star">★</span>
|
||||
}
|
||||
</div>
|
||||
<p class="testimonial-text">@t.Text</p>
|
||||
<div class="testimonial-author">
|
||||
<div class="author-avatar">@(string.IsNullOrEmpty(t.AuthorEmoji) ? "👩" : t.AuthorEmoji)</div>
|
||||
<div>
|
||||
<div class="author-name">@t.AuthorName</div>
|
||||
<div class="author-date">@t.Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ BLOG PREVIEW ══════ -->
|
||||
@if (Model.RecentPosts.Any())
|
||||
{
|
||||
<section id="blog-preview">
|
||||
<div class="container">
|
||||
<div class="fade-in" style="margin-bottom:3.5rem;">
|
||||
<span class="section-label">وبلاگ</span>
|
||||
<h2 class="section-title">آخرین مقالات</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">دانش و تجربه در حوزه زیبایی پوست</p>
|
||||
</div>
|
||||
<div class="blog-preview-grid">
|
||||
@foreach (var post in Model.RecentPosts)
|
||||
{
|
||||
<div class="post-card-prev fade-in">
|
||||
<div class="post-card-prev-img">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy" />
|
||||
}
|
||||
else { <span>📝</span> }
|
||||
</div>
|
||||
<div class="post-card-prev-body">
|
||||
@if (post.Category != null)
|
||||
{
|
||||
<span class="post-cat-label">@post.Category.Name</span>
|
||||
}
|
||||
<a href="/blog/@post.Slug" class="post-card-prev-title">@post.Title</a>
|
||||
<p class="post-card-prev-excerpt">@(post.Excerpt.Length > 120 ? post.Excerpt.Substring(0, 120) + "..." : post.Excerpt)</p>
|
||||
<div class="post-card-prev-meta">
|
||||
<span>🕐 @post.ReadingTimeMinutes دقیقه | 👁 @post.ViewCount</span>
|
||||
<a href="/blog/@post.Slug" class="read-more-link">ادامه مطلب ←</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="blog-preview-more">
|
||||
<a href="/blog" class="btn-secondary">مشاهده همه مقالات</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- ══════ FAQ ══════ -->
|
||||
<section id="faq">
|
||||
<div class="container">
|
||||
<div class="fade-in" style="margin-bottom:3rem;">
|
||||
<span class="section-label">سوالات متداول</span>
|
||||
<h2 class="section-title">پاسخ به پرسشهای رایج</h2>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="faq-list">
|
||||
@foreach (var faq in Model.Faqs)
|
||||
{
|
||||
<details class="faq-item">
|
||||
<summary>@faq.Question</summary>
|
||||
<div class="faq-answer">@faq.Answer</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ CONTACT ══════ -->
|
||||
<section id="contact">
|
||||
<div class="container">
|
||||
<div class="contact-grid">
|
||||
<div class="fade-in">
|
||||
<span class="section-label">تماس با ما</span>
|
||||
<h2 class="section-title">رزرو نوبت<br>و مشاوره</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc">برای رزرو نوبت یا دریافت مشاوره رایگان با ما در تماس باشید.</p>
|
||||
<div class="contact-info-list">
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("phone","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<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.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>تلفن تماس</strong>
|
||||
<p>@c.GetValueOrDefault("phone","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("email","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>ایمیل</strong>
|
||||
<p>@c.GetValueOrDefault("email","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("address","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>آدرس مطب</strong>
|
||||
<p>@c.GetValueOrDefault("address","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(c.GetValueOrDefault("hours","")))
|
||||
{
|
||||
<div class="contact-info-item">
|
||||
<div class="info-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<strong>ساعات کاری</strong>
|
||||
<p>@c.GetValueOrDefault("hours","")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="social-links">
|
||||
@if (!string.IsNullOrEmpty(ig))
|
||||
{
|
||||
<a href="@ig" class="social-link" target="_blank" rel="noopener" title="اینستاگرام">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"/>
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/>
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(wa))
|
||||
{
|
||||
<a href="@wa" class="social-link" target="_blank" rel="noopener" title="واتساپ">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(tg))
|
||||
{
|
||||
<a href="@tg" class="social-link" target="_blank" rel="noopener" title="تلگرام">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-form fade-in">
|
||||
<div class="form-title">رزرو نوبت آنلاین</div>
|
||||
<p class="form-sub">فرم زیر را پر کنید، در اسرع وقت با شما تماس میگیریم.</p>
|
||||
<form onsubmit="handleSubmit(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>نام</label>
|
||||
<input type="text" placeholder="نام شما" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>نام خانوادگی</label>
|
||||
<input type="text" placeholder="نام خانوادگی" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>شماره موبایل</label>
|
||||
<input type="tel" placeholder="09XX-XXX-XXXX" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>نوع خدمت مورد نظر</label>
|
||||
<select>
|
||||
<option value="" disabled selected>انتخاب خدمت</option>
|
||||
@foreach (var svc in Model.Services)
|
||||
{
|
||||
<option>@svc.Title</option>
|
||||
}
|
||||
<option>سایر</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>توضیحات (اختیاری)</label>
|
||||
<textarea placeholder="مشکل پوستی یا سوالات خود را اینجا بنویسید..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="form-submit">ارسال و رزرو نوبت</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Before/After toggle
|
||||
function toggleBA(wrap) {
|
||||
wrap.classList.toggle('show-after');
|
||||
const btns = wrap.querySelectorAll('.svc-ba-btn');
|
||||
btns[0].classList.toggle('active');
|
||||
btns[1].classList.toggle('active');
|
||||
}
|
||||
|
||||
// Tab buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('.form-submit');
|
||||
btn.textContent = '✓ درخواست شما ثبت شد';
|
||||
btn.style.background = '#2D7A4F';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'ارسال و رزرو نوبت';
|
||||
btn.style.background = '';
|
||||
e.target.reset();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Active nav link on scroll
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollY = window.scrollY;
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop - 100;
|
||||
const id = section.getAttribute('id');
|
||||
const link = document.querySelector(`header nav a[href="#${id}"]`);
|
||||
if (link && scrollY > sectionTop && scrollY <= sectionTop + section.offsetHeight) {
|
||||
document.querySelectorAll('header nav a').forEach(a => a.style.color = '');
|
||||
link.style.color = '#B8955A';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DrSousan.Api.Data;
|
||||
using DrSousan.Api.Models;
|
||||
|
||||
namespace DrSousan.Api.Pages;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
// Hero
|
||||
public Dictionary<string, string> Hero { get; private set; } = new();
|
||||
public Dictionary<string, string> About { get; private set; } = new();
|
||||
public Dictionary<string, string> Contact { get; private set; } = new();
|
||||
|
||||
// Collections
|
||||
public List<Service> Services { get; private set; } = new();
|
||||
public List<GalleryItem> Gallery { get; private set; } = new();
|
||||
public List<Testimonial> Testimonials { get; private set; } = new();
|
||||
public List<BlogPost> RecentPosts { get; private set; } = new();
|
||||
public List<Faq> Faqs { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var settings = await _db.SiteSettings.ToListAsync();
|
||||
|
||||
Hero = settings.Where(s => s.Section == "hero")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
About = settings.Where(s => s.Section == "about")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
Contact = settings.Where(s => s.Section == "contact")
|
||||
.ToDictionary(s => s.Key, s => s.Value);
|
||||
|
||||
Services = await _db.Services
|
||||
.Where(s => s.IsActive)
|
||||
.OrderBy(s => s.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Gallery = await _db.GalleryItems
|
||||
.Where(g => g.IsActive)
|
||||
.OrderBy(g => g.Order)
|
||||
.ToListAsync();
|
||||
|
||||
Testimonials = await _db.Testimonials
|
||||
.Where(t => t.IsActive)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
RecentPosts = await _db.BlogPosts
|
||||
.Include(p => p.Category)
|
||||
.Where(p => p.IsPublished)
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.Take(3)
|
||||
.ToListAsync();
|
||||
|
||||
Faqs = await _db.Faqs
|
||||
.Where(f => f.IsActive)
|
||||
.OrderBy(f => f.Order)
|
||||
.ToListAsync();
|
||||
|
||||
// Expose site name for layout
|
||||
var siteName = Hero.GetValueOrDefault("name", "دکتر سوسن آلطه");
|
||||
ViewData["SiteName"] = siteName;
|
||||
ViewData["Title"] = $"{siteName} | متخصص زیبایی پوست تهران";
|
||||
}
|
||||
|
||||
// ── Helpers for the view ─────────────────────────────────────────────────
|
||||
public static string Stars(int count)
|
||||
{
|
||||
var s = string.Concat(Enumerable.Repeat("⭐", count));
|
||||
return string.IsNullOrEmpty(s) ? "⭐⭐⭐⭐⭐" : s;
|
||||
}
|
||||
|
||||
public static string ServiceIcon(string title) => title switch
|
||||
{
|
||||
var t when t.Contains("لیزر") => "✨",
|
||||
var t when t.Contains("بوتاکس") || t.Contains("فیلر") => "💉",
|
||||
var t when t.Contains("مزو") => "💊",
|
||||
var t when t.Contains("پاکسازی") => "🧴",
|
||||
var t when t.Contains("مشاوره") => "🩺",
|
||||
var t when t.Contains("هایفو") || t.Contains("HIFU") => "🔬",
|
||||
var t when t.Contains("RF") => "⚡",
|
||||
_ => "🌟"
|
||||
};
|
||||
|
||||
public static string ServiceIconSvg(string title) => title switch
|
||||
{
|
||||
var t when t.Contains("لیزر") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>""",
|
||||
var t when t.Contains("بوتاکس") || t.Contains("فیلر") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>""",
|
||||
var t when t.Contains("مزو") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>""",
|
||||
var t when t.Contains("پاکسازی") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>""",
|
||||
var t when t.Contains("مشاوره") =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>""",
|
||||
_ =>
|
||||
"""<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>"""
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@RenderSection("Head", required: false)
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" onload="this.rel='stylesheet'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--gold: #B8955A;
|
||||
--gold-light: #D4B483;
|
||||
--gold-pale: #F5ECD8;
|
||||
--bg: #FAFAF7;
|
||||
--white: #FFFFFF;
|
||||
--dark: #1E1E1E;
|
||||
--mid: #5A5A5A;
|
||||
--light: #9A9A9A;
|
||||
--border: #E8E2D9;
|
||||
--section-bg: #F7F4EF;
|
||||
}
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: 'Vazirmatn', sans-serif; background: var(--bg); color: var(--dark); line-height: 1.8; direction: rtl; }
|
||||
a { text-decoration: none; color: inherit; }
|
||||
/* Navbar */
|
||||
header {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(250,250,247,0.92); backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border); padding: 0 2rem;
|
||||
height: 70px; display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.logo { font-size: 1rem; font-weight: 600; color: var(--gold); letter-spacing: 0.03em; text-decoration: none; }
|
||||
nav { display: flex; gap: 2rem; list-style: none; }
|
||||
nav a { text-decoration: none; color: var(--mid); font-size: 0.88rem; font-weight: 400; transition: color 0.25s; position: relative; }
|
||||
nav a::after { content: ''; position: absolute; bottom: -3px; right: 0; width: 0; height: 1px; background: var(--gold); transition: width 0.3s; }
|
||||
nav a:hover { color: var(--gold); }
|
||||
nav a:hover::after { width: 100%; }
|
||||
.nav-cta { background: var(--gold); color: var(--white) !important; padding: 0.45rem 1.2rem; border-radius: 50px; font-size: 0.85rem !important; transition: background 0.25s, transform 0.2s !important; }
|
||||
.nav-cta:hover { background: var(--gold-light) !important; transform: translateY(-1px); }
|
||||
.nav-cta::after { display: none !important; }
|
||||
.hamburger { display: none; flex-direction: column; gap: 5px; cursor: pointer; padding: 8px; border-radius: 8px; transition: background 0.2s; }
|
||||
.hamburger:hover { background: var(--gold-pale); }
|
||||
.hamburger span { display: block; width: 24px; height: 2px; background: var(--dark); border-radius: 2px; transition: transform 0.3s, opacity 0.3s; }
|
||||
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.hamburger.open span:nth-child(2) { opacity: 0; transform: scaleX(0); }
|
||||
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
/* Mobile nav drawer */
|
||||
.mobile-nav {
|
||||
display: none; position: fixed; top: 70px; right: 0; left: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0); z-index: 98; transition: background 0.25s;
|
||||
}
|
||||
.mobile-nav.open { display: block; background: rgba(0,0,0,0.35); }
|
||||
.mobile-nav-drawer {
|
||||
position: absolute; top: 0; right: 0; left: 0;
|
||||
background: var(--white); padding: 1.2rem 2rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
display: flex; flex-direction: column; gap: 0;
|
||||
transform: translateY(-8px); opacity: 0;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
}
|
||||
.mobile-nav.open .mobile-nav-drawer { transform: translateY(0); opacity: 1; }
|
||||
.mobile-nav-drawer a {
|
||||
display: block; padding: 0.85rem 0; color: var(--dark); font-size: 0.95rem;
|
||||
font-weight: 500; border-bottom: 1px solid var(--border); text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.mobile-nav-drawer a:last-child { border-bottom: none; padding-bottom: 0.5rem; }
|
||||
.mobile-nav-drawer a:hover { color: var(--gold); }
|
||||
.mobile-nav-drawer .nav-cta-m {
|
||||
margin-top: 1rem; background: var(--gold); color: var(--white) !important;
|
||||
text-align: center; padding: 0.75rem 1.5rem; border-radius: 50px;
|
||||
font-weight: 600; border-bottom: none !important;
|
||||
}
|
||||
/* Footer */
|
||||
footer { background: var(--dark); color: rgba(255,255,255,0.6); text-align: center; padding: 2.5rem 2rem; font-size: 0.85rem; }
|
||||
footer a { color: var(--gold-light); text-decoration: none; }
|
||||
/* Shared buttons */
|
||||
.btn-primary {
|
||||
background: var(--gold); color: var(--white); border: none; padding: 0.8rem 2rem;
|
||||
border-radius: 50px; font-family: 'Vazirmatn', sans-serif; font-size: 0.95rem; font-weight: 500;
|
||||
cursor: pointer; text-decoration: none; display: inline-block;
|
||||
transition: background 0.25s, transform 0.2s, box-shadow 0.25s;
|
||||
box-shadow: 0 4px 18px rgba(184,149,90,0.25);
|
||||
}
|
||||
.btn-primary:hover { background: var(--gold-light); transform: translateY(-2px); box-shadow: 0 8px 24px rgba(184,149,90,0.35); }
|
||||
.btn-secondary {
|
||||
background: transparent; color: var(--gold); border: 1.5px solid var(--gold);
|
||||
padding: 0.78rem 1.8rem; border-radius: 50px; font-family: 'Vazirmatn', sans-serif;
|
||||
font-size: 0.95rem; font-weight: 500; cursor: pointer; text-decoration: none;
|
||||
display: inline-block; transition: all 0.25s;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--gold); color: var(--white); transform: translateY(-2px); }
|
||||
/* Shared */
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 2rem; }
|
||||
section { padding: 6rem 0; }
|
||||
.section-label { font-size: 0.78rem; font-weight: 600; letter-spacing: 0.1em; color: var(--gold); text-transform: uppercase; margin-bottom: 0.8rem; display: block; }
|
||||
.section-title { font-size: clamp(1.6rem, 3vw, 2.3rem); font-weight: 700; color: var(--dark); margin-bottom: 1.2rem; line-height: 1.35; }
|
||||
.section-desc { font-size: 1rem; color: var(--mid); max-width: 560px; line-height: 1.9; }
|
||||
.divider { width: 60px; height: 3px; background: linear-gradient(90deg, var(--gold), var(--gold-pale)); border-radius: 2px; margin: 1.2rem 0 2.5rem; }
|
||||
/* Animations */
|
||||
@@keyframes fadeUp { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||
.fade-in { opacity: 0; transform: translateY(24px); transition: opacity 0.7s ease, transform 0.7s ease; }
|
||||
.fade-in.visible { opacity: 1; transform: translateY(0); }
|
||||
/* Mobile nav */
|
||||
@@media (max-width: 900px) {
|
||||
nav { display: none; }
|
||||
.hamburger { display: flex; }
|
||||
}
|
||||
/* Small screen tweaks */
|
||||
@@media (max-width: 480px) {
|
||||
header { padding: 0 1rem; }
|
||||
.logo { font-size: 0.88rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a class="logo" href="/">@(ViewData["SiteName"] ?? "دکتر سوسن آلطه")</a>
|
||||
<nav>
|
||||
<a href="/#about">درباره من</a>
|
||||
<a href="/#services">خدمات</a>
|
||||
<a href="/#gallery">گالری</a>
|
||||
<a href="/blog">وبلاگ</a>
|
||||
<a href="/#testimonials">نظرات</a>
|
||||
<a href="/#contact" class="nav-cta">رزرو نوبت</a>
|
||||
</nav>
|
||||
<div class="hamburger" id="hamburger" onclick="toggleMenu()" aria-label="منو" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Mobile nav drawer (separate from header so it can overlay body) -->
|
||||
<div class="mobile-nav" id="mobileNav" onclick="closeMobileNavOnOverlay(event)">
|
||||
<div class="mobile-nav-drawer">
|
||||
<a href="/#about" onclick="closeMenu()">درباره من</a>
|
||||
<a href="/#services" onclick="closeMenu()">خدمات</a>
|
||||
<a href="/#gallery" onclick="closeMenu()">گالری</a>
|
||||
<a href="/blog" onclick="closeMenu()">وبلاگ</a>
|
||||
<a href="/#testimonials" onclick="closeMenu()">نظرات</a>
|
||||
<a href="/#contact" onclick="closeMenu()" class="nav-cta-m">رزرو نوبت</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<footer>
|
||||
<p>© @DateTime.Now.Year @(ViewData["SiteName"] ?? "دکتر سوسن آلطه") — تمامی حقوق محفوظ است.</p>
|
||||
<p style="margin-top:0.5rem; font-size:0.78rem; opacity:0.5;">طراحی شده با ❤ برای سلامت و زیبایی شما</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const _ham = document.getElementById('hamburger');
|
||||
const _nav = document.getElementById('mobileNav');
|
||||
|
||||
function toggleMenu() {
|
||||
const open = _nav.classList.toggle('open');
|
||||
_ham.classList.toggle('open', open);
|
||||
_ham.setAttribute('aria-expanded', String(open));
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
}
|
||||
function closeMenu() {
|
||||
_nav.classList.remove('open');
|
||||
_ham.classList.remove('open');
|
||||
_ham.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
function closeMobileNavOnOverlay(e) {
|
||||
// close only when clicking the dim overlay, not the drawer itself
|
||||
if (e.target === _nav) closeMenu();
|
||||
}
|
||||
// close on Escape key
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
|
||||
// close when viewport widens past mobile breakpoint
|
||||
window.addEventListener('resize', () => { if (window.innerWidth > 900) closeMenu(); });
|
||||
|
||||
// Scroll fade-in
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
||||
}, { threshold: 0.1 });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
|
||||
});
|
||||
</script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
@using DrSousan.Api.Models
|
||||
@using DrSousan.Api.Data
|
||||
@namespace DrSousan.Api.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:18342",
|
||||
"sslPort": 44378
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"applicationUrl": "https://localhost:7001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Data Source=drsousan.db"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "DrSousanSecretKey2024!ChangeThisInProduction!MinLength32Chars",
|
||||
"Issuer": "DrSousanApi",
|
||||
"Audience": "DrSousanAdmin"
|
||||
},
|
||||
"Admin": {
|
||||
"Username": "admin",
|
||||
"Password": "admin123"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
/* site.css — minimal shared overrides only; all page CSS is inline in each Razor page */
|
||||
/* The _Layout.cshtml already defines all base/reset/nav/footer styles inline */
|
||||
Reference in New Issue
Block a user