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
+22
View File
@@ -0,0 +1,22 @@
@page
@model AsadiTools.Pages.Admin.Blog.AdminBlogCreateModel
@{ ViewData["Title"] = "نوشته جدید"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8 max-w-3xl">
<div class="flex items-center gap-3 mb-8">
<a href="/Admin/Blog" class="text-gray-400 hover:text-gray-600 text-sm">← بازگشت</a>
<h1 class="text-2xl font-extrabold text-gray-900">نوشته جدید</h1>
</div>
<form method="post" class="bg-white rounded-2xl border border-gray-100 p-6">
@Html.AntiForgeryToken()
@await Html.PartialAsync("_BlogFormFields", Model.Input)
<div class="flex gap-3 mt-6 pt-5 border-t">
<button type="submit" class="flex-1 bg-blue-700 text-white py-3 rounded-xl font-bold hover:bg-blue-800 transition-colors">
ذخیره مقاله
</button>
<a href="/Admin/Blog" class="px-5 border border-gray-200 rounded-xl text-sm text-gray-600 hover:bg-gray-50 transition-colors flex items-center">
انصراف
</a>
</div>
</form>
</div>
+58
View File
@@ -0,0 +1,58 @@
using AsadiTools.Data;
using AsadiTools.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace AsadiTools.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class AdminBlogCreateModel(AppDbContext db) : PageModel
{
[BindProperty] public BlogPostInput Input { get; set; } = new();
public void OnGet() { ViewData["Title"] = "نوشته جدید"; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
var slug = string.IsNullOrWhiteSpace(Input.Slug)
? Input.Title.ToLower()
.Replace(" ", "-")
.Replace("،", "")
.Replace(".", "-")
: Input.Slug.Trim();
var now = DateTime.Now;
db.BlogPosts.Add(new BlogPost
{
Title = Input.Title,
Slug = slug,
Content = Input.Content,
Excerpt = Input.Excerpt,
MetaDescription = Input.MetaDescription,
FeaturedImage = string.IsNullOrWhiteSpace(Input.FeaturedImage) ? null : Input.FeaturedImage,
Tags = Input.Tags,
IsPublished = Input.IsPublished,
PublishedAt = Input.IsPublished ? now : null,
CreatedAt = now,
UpdatedAt = now,
});
await db.SaveChangesAsync();
return RedirectToPage("/Admin/Blog/Index");
}
}
public class BlogPostInput
{
[Required] public string Title { get; set; } = string.Empty;
public string? Slug { get; set; }
[Required] public string Content { get; set; } = string.Empty;
public string? Excerpt { get; set; }
public string? MetaDescription { get; set; }
public string? FeaturedImage { get; set; }
public string? Tags { get; set; }
public bool IsPublished { get; set; }
}
+23
View File
@@ -0,0 +1,23 @@
@page
@model AsadiTools.Pages.Admin.Blog.AdminBlogEditModel
@{ Layout = "_AdminLayout"; }
<div class="p-6 md:p-8 max-w-3xl">
<div class="flex items-center gap-3 mb-8">
<a href="/Admin/Blog" class="text-gray-400 hover:text-gray-600 text-sm">← بازگشت</a>
<h1 class="text-2xl font-extrabold text-gray-900">ویرایش مقاله</h1>
</div>
<form method="post" class="bg-white rounded-2xl border border-gray-100 p-6">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.PostId" />
@await Html.PartialAsync("_BlogFormFields", Model.Input)
<div class="flex gap-3 mt-6 pt-5 border-t">
<button type="submit" class="flex-1 bg-blue-700 text-white py-3 rounded-xl font-bold hover:bg-blue-800 transition-colors">
ذخیره تغییرات
</button>
<a href="/Admin/Blog" class="px-5 border border-gray-200 rounded-xl text-sm text-gray-600 hover:bg-gray-50 transition-colors flex items-center">
انصراف
</a>
</div>
</form>
</div>
+57
View File
@@ -0,0 +1,57 @@
using AsadiTools.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class AdminBlogEditModel(AppDbContext db) : PageModel
{
[BindProperty] public BlogPostInput Input { get; set; } = new();
public int PostId { get; private set; }
public async Task<IActionResult> OnGetAsync(int id)
{
var post = await db.BlogPosts.FindAsync(id);
if (post is null) return NotFound();
PostId = id;
Input = new BlogPostInput
{
Title = post.Title,
Slug = post.Slug,
Content = post.Content,
Excerpt = post.Excerpt,
MetaDescription = post.MetaDescription,
FeaturedImage = post.FeaturedImage,
Tags = post.Tags,
IsPublished = post.IsPublished,
};
ViewData["Title"] = "ویرایش: " + post.Title;
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid) { PostId = id; return Page(); }
var post = await db.BlogPosts.FindAsync(id);
if (post is null) return NotFound();
var wasUnpublished = !post.IsPublished;
post.Title = Input.Title;
post.Slug = string.IsNullOrWhiteSpace(Input.Slug) ? post.Slug : Input.Slug.Trim();
post.Content = Input.Content;
post.Excerpt = Input.Excerpt;
post.MetaDescription = Input.MetaDescription;
post.FeaturedImage = string.IsNullOrWhiteSpace(Input.FeaturedImage) ? null : Input.FeaturedImage;
post.Tags = Input.Tags;
post.IsPublished = Input.IsPublished;
post.UpdatedAt = DateTime.Now;
if (Input.IsPublished && wasUnpublished)
post.PublishedAt = DateTime.Now;
await db.SaveChangesAsync();
return RedirectToPage("/Admin/Blog/Index");
}
}
+86
View File
@@ -0,0 +1,86 @@
@page
@model AsadiTools.Pages.Admin.Blog.AdminBlogIndexModel
@{ ViewData["Title"] = "مدیریت بلاگ"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8">
<div class="flex items-center justify-between mb-8">
<h1 class="text-2xl font-extrabold text-gray-900">مدیریت بلاگ</h1>
<a href="/Admin/Blog/Create"
class="bg-blue-700 text-white px-5 py-2.5 rounded-xl font-bold hover:bg-blue-800 transition-colors flex items-center gap-2">
✏️ نوشته جدید
</a>
</div>
@if (!Model.Posts.Any())
{
<div class="text-center py-20 text-gray-400">
<div class="text-5xl mb-4">📝</div>
<p>هنوز مقاله‌ای ثبت نشده</p>
</div>
}
else
{
<div class="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="p-4 text-right font-medium text-gray-500">عنوان</th>
<th class="p-4 text-right font-medium text-gray-500 hidden md:table-cell">برچسب‌ها</th>
<th class="p-4 text-right font-medium text-gray-500">وضعیت</th>
<th class="p-4 text-right font-medium text-gray-500 hidden lg:table-cell">تاریخ</th>
<th class="p-4 text-right font-medium text-gray-500">عملیات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@foreach (var post in Model.Posts)
{
<tr class="hover:bg-gray-50">
<td class="p-4">
<div class="font-medium text-gray-900 line-clamp-1">@post.Title</div>
<div class="text-xs text-gray-400 font-mono mt-0.5">@post.EffectiveSlug</div>
</td>
<td class="p-4 hidden md:table-cell">
<div class="flex flex-wrap gap-1">
@foreach (var tag in post.TagList.Take(3))
{
<span class="text-xs bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">@tag</span>
}
</div>
</td>
<td class="p-4">
<form method="post" asp-page-handler="TogglePublish" class="inline">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@post.Id" />
<button type="submit"
class="text-xs px-2.5 py-1 rounded-full font-medium border transition-colors @(post.IsPublished ? "bg-green-100 text-green-700 border-green-200 hover:bg-green-200" : "bg-gray-100 text-gray-500 border-gray-200 hover:bg-gray-200")">
@(post.IsPublished ? "✅ منتشر" : "⏸ پیش‌نویس")
</button>
</form>
</td>
<td class="p-4 hidden lg:table-cell text-gray-400 text-xs">
@SiteData.ToJalali(post.CreatedAt)
</td>
<td class="p-4">
<div class="flex items-center gap-2">
<a href="/Admin/Blog/Edit?id=@post.Id"
class="text-blue-600 hover:underline text-xs font-medium">ویرایش</a>
@if (post.IsPublished)
{
<a href="/blog/@post.EffectiveSlug" target="_blank"
class="text-gray-400 hover:text-gray-600 text-xs">مشاهده</a>
}
<form method="post" asp-page-handler="Delete"
onsubmit="return confirm('این مقاله حذف شود؟')" class="inline">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@post.Id" />
<button type="submit" class="text-red-400 hover:text-red-600 text-xs">حذف</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
+41
View File
@@ -0,0 +1,41 @@
using AsadiTools.Data;
using AsadiTools.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace AsadiTools.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class AdminBlogIndexModel(AppDbContext db) : PageModel
{
public List<BlogPost> Posts { get; private set; } = [];
public async Task OnGetAsync()
{
Posts = await db.BlogPosts.OrderByDescending(p => p.CreatedAt).ToListAsync();
ViewData["Title"] = "مدیریت بلاگ";
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var post = await db.BlogPosts.FindAsync(id);
if (post is not null) { db.BlogPosts.Remove(post); await db.SaveChangesAsync(); }
return RedirectToPage();
}
public async Task<IActionResult> OnPostTogglePublishAsync(int id)
{
var post = await db.BlogPosts.FindAsync(id);
if (post is not null)
{
post.IsPublished = !post.IsPublished;
if (post.IsPublished && post.PublishedAt is null)
post.PublishedAt = DateTime.Now;
post.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
}
return RedirectToPage();
}
}
+55
View File
@@ -0,0 +1,55 @@
@model AsadiTools.Pages.Admin.Blog.BlogPostInput
@{
var cls = "w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500";
}
<div class="space-y-5">
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">عنوان مقاله <span class="text-red-500">*</span></label>
<input asp-for="Title" class="@cls" placeholder="مثال: راهنمای تعمیر فرز برقی" />
<span asp-validation-for="Title" class="text-red-500 text-xs"></span>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">اسلاگ (URL)</label>
<input asp-for="Slug" class="@cls" dir="ltr" placeholder="carbon-brush-guide (خودکار از عنوان)" />
<p class="text-xs text-gray-400 mt-1">URL صفحه: /blog/اسلاگ — اگر خالی بماند از عنوان ساخته می‌شود</p>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">چکیده (برای لیست بلاگ)</label>
<textarea asp-for="Excerpt" rows="2" class="@cls resize-none" placeholder="خلاصه کوتاه مقاله..."></textarea>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">توضیح متا (SEO)</label>
<textarea asp-for="MetaDescription" rows="2" class="@cls resize-none" placeholder="توضیح برای گوگل (حداکثر ۱۶۰ کاراکتر)..."></textarea>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">آدرس تصویر شاخص (URL)</label>
<input asp-for="FeaturedImage" class="@cls" dir="ltr" placeholder="https://..." />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">برچسب‌ها (با کاما جدا شوند)</label>
<input asp-for="Tags" class="@cls" placeholder="تعمیر فرز,کاربن,دیوالت" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">محتوا (HTML) <span class="text-red-500">*</span></label>
<textarea asp-for="Content" rows="20" class="@cls resize-y font-mono text-xs leading-relaxed" dir="auto"
placeholder="<h2>عنوان بخش</h2>&#10;<p>متن مقاله...</p>"></textarea>
<p class="text-xs text-gray-400 mt-1">محتوا به صورت HTML نوشته شود. از تگ‌های h2، h3، p، ul، li، strong استفاده کنید.</p>
<span asp-validation-for="Content" class="text-red-500 text-xs"></span>
</div>
<div class="flex items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-100">
<input asp-for="IsPublished" type="checkbox" class="w-4 h-4 rounded accent-blue-600" />
<div>
<label asp-for="IsPublished" class="font-medium text-gray-800 text-sm cursor-pointer">انتشار فوری</label>
<p class="text-xs text-gray-400">اگر تیک بزنید، مقاله بلافاصله در سایت نمایش داده می‌شود.</p>
</div>
</div>
</div>