Initial commit — AsadiTools v1.0
Full ASP.NET Core 10 Razor Pages app for آساد ابزار tool repair shop in Karaj, Iran (official DeWalt representative). Features: - Homepage, Services, DeWalt page, Shop (pagination + images) - 10 brand SEO pages (/brands/*) with rich Persian content + FAQ schema - Blog engine with admin management (/blog, /Admin/Blog) - Cart, Checkout, Contact (OpenStreetMap embed) - Admin panel: Products CRUD, Orders, Blog, Change Password - Jalali date formatting, product images, SiteData centralised contact - Docker + docker-compose with healthcheck - Gitea CI/CD via .gitea/workflows/ci-cd.yml (NuGet through Nexus mirror) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
@page
|
||||
@model AsadiTools.Pages.Blog.BlogIndexModel
|
||||
@{ Layout = "_Layout"; }
|
||||
|
||||
<div class="bg-blue-800 text-white py-12 px-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<nav class="flex items-center gap-2 text-sm text-blue-300 mb-4">
|
||||
<a href="/" class="hover:text-white">خانه</a><span>/</span>
|
||||
<span class="text-white">بلاگ</span>
|
||||
</nav>
|
||||
<h1 class="text-3xl font-extrabold mb-2">بلاگ آساد ابزار</h1>
|
||||
<p class="text-blue-200">راهنما، نکات فنی و مقالات تخصصی تعمیر ابزار برقی</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 py-10">
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Tag))
|
||||
{
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm font-bold">برچسب: @Model.Tag</span>
|
||||
<a href="/blog" class="text-sm text-gray-400 hover:text-gray-600">× حذف فیلتر</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.Posts.Any())
|
||||
{
|
||||
<div class="text-center py-20 text-gray-400">
|
||||
<div class="text-5xl mb-4">📝</div>
|
||||
<p>مقالهای یافت نشد.</p>
|
||||
<a href="/blog" class="text-blue-600 text-sm mt-2 block hover:underline">مشاهده همه مقالات</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<article class="bg-white rounded-2xl overflow-hidden border border-gray-100 hover:shadow-lg transition-shadow flex flex-col">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<a href="/blog/@post.EffectiveSlug" class="block overflow-hidden" style="height:200px">
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" loading="lazy"
|
||||
class="w-full h-full object-cover hover:scale-105 transition-transform duration-500" />
|
||||
</a>
|
||||
}
|
||||
<div class="p-5 flex flex-col flex-1">
|
||||
@if (post.TagList.Any())
|
||||
{
|
||||
<div class="flex flex-wrap gap-1.5 mb-3">
|
||||
@foreach (var tag in post.TagList.Take(3))
|
||||
{
|
||||
<a href="/blog?tag=@Uri.EscapeDataString(tag)"
|
||||
class="text-xs bg-blue-50 text-blue-600 px-2 py-0.5 rounded-full hover:bg-blue-100 transition-colors">@tag</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<h2 class="font-bold text-lg text-gray-900 mb-2 leading-snug hover:text-blue-700 transition-colors">
|
||||
<a href="/blog/@post.EffectiveSlug">@post.Title</a>
|
||||
</h2>
|
||||
@if (!string.IsNullOrEmpty(post.Excerpt))
|
||||
{
|
||||
<p class="text-sm text-gray-500 leading-7 mb-4 line-clamp-3 flex-1">@post.Excerpt</p>
|
||||
}
|
||||
<div class="flex items-center justify-between mt-auto pt-3 border-t border-gray-50">
|
||||
<span class="text-xs text-gray-400">📅 @post.DisplayDate</span>
|
||||
<a href="/blog/@post.EffectiveSlug"
|
||||
class="text-sm text-blue-600 font-medium hover:underline">ادامه مطلب ›</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.TotalPages > 1)
|
||||
{
|
||||
<div class="flex justify-center items-center gap-2 mt-10">
|
||||
@if (Model.CurrentPage > 1)
|
||||
{
|
||||
<a href="/blog?page=@(Model.CurrentPage - 1)@(Model.Tag != null ? "&tag=" + Uri.EscapeDataString(Model.Tag) : "")"
|
||||
class="px-4 py-2 rounded-xl border border-gray-200 text-sm text-gray-600 hover:border-blue-400 hover:text-blue-700 transition-colors">‹ قبلی</a>
|
||||
}
|
||||
@for (var i = Math.Max(1, Model.CurrentPage - 2); i <= Math.Min(Model.TotalPages, Model.CurrentPage + 2); i++)
|
||||
{
|
||||
<a href="/blog?page=@i@(Model.Tag != null ? "&tag=" + Uri.EscapeDataString(Model.Tag) : "")"
|
||||
class="px-4 py-2 rounded-xl border text-sm transition-colors @(i == Model.CurrentPage ? "bg-blue-700 text-white border-blue-700" : "border-gray-200 text-gray-600 hover:border-blue-400")">@i</a>
|
||||
}
|
||||
@if (Model.CurrentPage < Model.TotalPages)
|
||||
{
|
||||
<a href="/blog?page=@(Model.CurrentPage + 1)@(Model.Tag != null ? "&tag=" + Uri.EscapeDataString(Model.Tag) : "")"
|
||||
class="px-4 py-2 rounded-xl border border-gray-200 text-sm text-gray-600 hover:border-blue-400 hover:text-blue-700 transition-colors">بعدی ›</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
using AsadiTools.Data;
|
||||
using AsadiTools.Models;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AsadiTools.Pages.Blog;
|
||||
|
||||
public class BlogIndexModel(AppDbContext db) : PageModel
|
||||
{
|
||||
public const int PageSize = 6;
|
||||
public List<BlogPost> Posts { get; private set; } = [];
|
||||
public int CurrentPage { get; private set; } = 1;
|
||||
public int TotalPages { get; private set; }
|
||||
public string? Tag { get; private set; }
|
||||
|
||||
public async Task OnGetAsync(string? tag, int page = 1)
|
||||
{
|
||||
Tag = tag;
|
||||
|
||||
var q = db.BlogPosts.Where(p => p.IsPublished);
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
q = q.Where(p => p.Tags != null && p.Tags.Contains(tag));
|
||||
|
||||
var total = await q.CountAsync();
|
||||
TotalPages = (int)Math.Ceiling(total / (double)PageSize);
|
||||
CurrentPage = Math.Clamp(page, 1, Math.Max(1, TotalPages));
|
||||
|
||||
Posts = await q.OrderByDescending(p => p.PublishedAt)
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
ViewData["Title"] = "بلاگ آساد ابزار — راهنما و مقالات تعمیر ابزار";
|
||||
ViewData["Description"] = "مقالات تخصصی تعمیر و نگهداری ابزار برقی. راهنمای خرید، نکات فنی و اخبار صنعت ابزار از آساد ابزار کرج.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
@page "/blog/{slug}"
|
||||
@model AsadiTools.Pages.Blog.BlogPostModel
|
||||
@{ Layout = "_Layout"; var p = Model.Post!; var c = SiteData.Company; }
|
||||
|
||||
@section Head {
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "BlogPosting",
|
||||
"headline": "@p.Title.Replace("\"","'")",
|
||||
"description": "@((p.MetaDescription ?? p.Excerpt ?? "").Replace("\"","'"))",
|
||||
"image": "@(p.FeaturedImage ?? "")",
|
||||
"datePublished": "@(p.PublishedAt?.ToString("yyyy-MM-dd") ?? p.CreatedAt.ToString("yyyy-MM-dd"))",
|
||||
"dateModified": "@p.UpdatedAt.ToString("yyyy-MM-dd")",
|
||||
"author": { "@@type": "Organization", "name": "آساد ابزار کرج" },
|
||||
"publisher": { "@@type": "Organization", "name": "آساد ابزار کرج", "logo": { "@@type": "ImageObject", "url": "" } },
|
||||
"mainEntityOfPage": { "@@type": "WebPage", "@@id": "/blog/@p.EffectiveSlug" }
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 py-10">
|
||||
<div class="grid lg:grid-cols-3 gap-10">
|
||||
|
||||
<!-- ── Article ──────────────────────────────────────────────────── -->
|
||||
<article class="lg:col-span-2">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex items-center gap-2 text-sm text-gray-400 mb-6">
|
||||
<a href="/" class="hover:text-blue-600">خانه</a><span>/</span>
|
||||
<a href="/blog" class="hover:text-blue-600">بلاگ</a><span>/</span>
|
||||
<span class="text-gray-700 line-clamp-1">@p.Title</span>
|
||||
</nav>
|
||||
|
||||
<!-- Tags -->
|
||||
@if (p.TagList.Any())
|
||||
{
|
||||
<div class="flex flex-wrap gap-1.5 mb-4">
|
||||
@foreach (var tag in p.TagList)
|
||||
{
|
||||
<a href="/blog?tag=@Uri.EscapeDataString(tag)"
|
||||
class="text-xs bg-blue-50 text-blue-600 px-2.5 py-1 rounded-full hover:bg-blue-100 transition-colors">@tag</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<h1 class="text-3xl font-extrabold text-gray-900 leading-tight mb-4">@p.Title</h1>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400 mb-8 pb-8 border-b">
|
||||
<span>📅 @p.DisplayDate</span>
|
||||
<span>✍️ آساد ابزار کرج</span>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(p.FeaturedImage))
|
||||
{
|
||||
<div class="rounded-2xl overflow-hidden mb-8" style="max-height:420px">
|
||||
<img src="@p.FeaturedImage" alt="@p.Title" class="w-full h-full object-cover" loading="eager" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="prose prose-lg max-w-none text-gray-700 leading-8
|
||||
[&_h2]:text-2xl [&_h2]:font-extrabold [&_h2]:text-gray-900 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:pb-2 [&_h2]:border-b
|
||||
[&_h3]:text-xl [&_h3]:font-bold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-3
|
||||
[&_p]:mb-4 [&_p]:leading-8
|
||||
[&_ul]:mb-4 [&_ul]:space-y-2 [&_ul]:list-disc [&_ul]:pr-6
|
||||
[&_ol]:mb-4 [&_ol]:space-y-2 [&_ol]:list-decimal [&_ol]:pr-6
|
||||
[&_li]:leading-7
|
||||
[&_blockquote]:border-r-4 [&_blockquote]:border-blue-400 [&_blockquote]:pr-4 [&_blockquote]:italic [&_blockquote]:text-gray-600 [&_blockquote]:my-6
|
||||
[&_strong]:font-bold [&_strong]:text-gray-900
|
||||
[&_table]:w-full [&_table]:border-collapse [&_table]:my-6
|
||||
[&_th]:bg-gray-100 [&_th]:p-3 [&_th]:text-right [&_th]:font-bold [&_th]:border [&_th]:border-gray-200
|
||||
[&_td]:p-3 [&_td]:border [&_td]:border-gray-200 [&_td]:text-right">
|
||||
@Html.Raw(p.Content)
|
||||
</div>
|
||||
|
||||
<!-- Related -->
|
||||
@if (Model.RelatedPosts.Any())
|
||||
{
|
||||
<div class="mt-12 pt-8 border-t">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-5">مقالات مرتبط</h2>
|
||||
<div class="grid sm:grid-cols-3 gap-4">
|
||||
@foreach (var rp in Model.RelatedPosts)
|
||||
{
|
||||
<a href="/blog/@rp.EffectiveSlug"
|
||||
class="group bg-white rounded-xl border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
@if (!string.IsNullOrEmpty(rp.FeaturedImage))
|
||||
{
|
||||
<div style="height:120px" class="overflow-hidden">
|
||||
<img src="@rp.FeaturedImage" alt="@rp.Title" loading="lazy"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
</div>
|
||||
}
|
||||
<div class="p-3">
|
||||
<p class="text-sm font-medium text-gray-800 leading-snug line-clamp-2 group-hover:text-blue-700">@rp.Title</p>
|
||||
<p class="text-xs text-gray-400 mt-1">@rp.DisplayDate</p>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
|
||||
<!-- ── Sidebar ───────────────────────────────────────────────────── -->
|
||||
<aside class="space-y-5 lg:sticky lg:top-24 lg:self-start">
|
||||
<!-- CTA -->
|
||||
<div class="bg-blue-700 text-white rounded-2xl p-6 text-center">
|
||||
<div class="text-3xl mb-2">🔧</div>
|
||||
<h3 class="font-extrabold mb-2">تعمیر ابزار در کرج</h3>
|
||||
<p class="text-blue-200 text-sm mb-5">تشخیص رایگان • ضمانت ۳ ماهه</p>
|
||||
<a href="tel:@c.TelPhone"
|
||||
class="block bg-white text-blue-700 font-bold py-3 rounded-xl hover:opacity-90 mb-3 transition-opacity">
|
||||
📞 @c.Phone
|
||||
</a>
|
||||
<a href="https://wa.me/@c.Whatsapp" target="_blank"
|
||||
class="block bg-green-500 text-white font-bold py-3 rounded-xl hover:bg-green-600 transition-colors">
|
||||
💬 واتساپ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tags cloud -->
|
||||
@if (p.TagList.Any())
|
||||
{
|
||||
<div class="bg-white rounded-2xl border border-gray-100 p-5">
|
||||
<h3 class="font-bold text-gray-900 mb-3 text-sm">برچسبها</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach (var tag in p.TagList)
|
||||
{
|
||||
<a href="/blog?tag=@Uri.EscapeDataString(tag)"
|
||||
class="text-xs bg-gray-100 text-gray-600 px-2.5 py-1 rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors">@tag</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<a href="/blog" class="block bg-gray-50 border border-gray-200 rounded-2xl p-5 hover:shadow-md transition-shadow text-center">
|
||||
<span class="text-xl block mb-1">📖</span>
|
||||
<span class="font-bold text-gray-800 text-sm">مشاهده همه مقالات</span>
|
||||
</a>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
using AsadiTools.Data;
|
||||
using AsadiTools.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AsadiTools.Pages.Blog;
|
||||
|
||||
public class BlogPostModel(AppDbContext db) : PageModel
|
||||
{
|
||||
public BlogPost? Post { get; private set; }
|
||||
public List<BlogPost> RelatedPosts { get; private set; } = [];
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string slug)
|
||||
{
|
||||
Post = await db.BlogPosts
|
||||
.Where(p => p.IsPublished && p.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// Fallback: numeric slug = post ID
|
||||
if (Post is null && int.TryParse(slug, out var id))
|
||||
Post = await db.BlogPosts.Where(p => p.IsPublished && p.Id == id).FirstOrDefaultAsync();
|
||||
|
||||
if (Post is null) return NotFound();
|
||||
|
||||
// Related: same tag(s)
|
||||
var tags = Post.TagList;
|
||||
if (tags.Length > 0)
|
||||
{
|
||||
RelatedPosts = await db.BlogPosts
|
||||
.Where(p => p.IsPublished && p.Id != Post.Id && p.Tags != null)
|
||||
.ToListAsync();
|
||||
RelatedPosts = RelatedPosts
|
||||
.Where(p => p.TagList.Any(t => tags.Contains(t)))
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.Take(3)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
ViewData["Title"] = Post.Title + " | آساد ابزار";
|
||||
ViewData["Description"] = Post.MetaDescription ?? Post.Excerpt ?? Post.Title;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user