Initial commit — AsadiTools v1.0
CI/CD / CI — dotnet build (push) Successful in 44s
CI/CD / Deploy — docker compose (push) Failing after 1s

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:
Soroush Asadi
2026-06-01 22:08:43 +03:30
commit f97f891d67
146 changed files with 88128 additions and 0 deletions
+96
View File
@@ -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>
+36
View File
@@ -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"] = "مقالات تخصصی تعمیر و نگهداری ابزار برقی. راهنمای خرید، نکات فنی و اخبار صنعت ابزار از آساد ابزار کرج.";
}
}
+143
View File
@@ -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>
+44
View File
@@ -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();
}
}