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,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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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> <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>
|
||||
Reference in New Issue
Block a user