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>
+50
View File
@@ -0,0 +1,50 @@
@page
@model AsadiTools.Pages.Admin.ChangePassword.ChangePasswordModel
@{ ViewData["Title"] = "تغییر رمز عبور"; Layout = "_AdminLayout"; }
@{
var inputCls = "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="p-6 md:p-8 max-w-lg">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">تغییر رمز عبور</h1>
@if (Model.Success)
{
<div class="bg-green-50 border border-green-200 text-green-700 px-5 py-4 rounded-xl mb-6 flex items-center gap-2">
✅ رمز عبور با موفقیت تغییر یافت.
</div>
}
<form method="post" class="bg-white rounded-2xl border border-gray-100 p-6 space-y-5">
@Html.AntiForgeryToken()
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3 rounded-xl">@Model.ErrorMessage</div>
}
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">رمز عبور فعلی <span class="text-red-500">*</span></label>
<input asp-for="Input.CurrentPassword" type="password" class="@inputCls" />
<span asp-validation-for="Input.CurrentPassword" class="text-red-500 text-xs"></span>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">رمز عبور جدید <span class="text-red-500">*</span></label>
<input asp-for="Input.NewPassword" type="password" class="@inputCls" />
<span asp-validation-for="Input.NewPassword" class="text-red-500 text-xs"></span>
<p class="text-xs text-gray-400 mt-1">حداقل ۶ کاراکتر</p>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">تکرار رمز عبور جدید <span class="text-red-500">*</span></label>
<input asp-for="Input.ConfirmPassword" type="password" class="@inputCls" />
<span asp-validation-for="Input.ConfirmPassword" class="text-red-500 text-xs"></span>
</div>
<button type="submit" class="w-full bg-blue-700 text-white py-3 rounded-xl font-bold hover:bg-blue-800 transition-colors">
🔑 تغییر رمز عبور
</button>
</form>
</div>
@@ -0,0 +1,51 @@
using AsadiTools.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
namespace AsadiTools.Pages.Admin.ChangePassword;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class ChangePasswordModel(AppDbContext db) : PageModel
{
[BindProperty] public ChangePasswordInput Input { get; set; } = new();
public string? ErrorMessage { get; private set; }
public bool Success { get; private set; }
public void OnGet() { }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var user = await db.AdminUsers.FindAsync(userId);
if (user is null) return RedirectToPage("/Admin/Login");
if (!BCrypt.Net.BCrypt.Verify(Input.CurrentPassword, user.PasswordHash))
{
ErrorMessage = "رمز عبور فعلی اشتباه است";
return Page();
}
if (Input.NewPassword != Input.ConfirmPassword)
{
ErrorMessage = "رمز عبور جدید و تکرار آن یکسان نیستند";
return Page();
}
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(Input.NewPassword);
await db.SaveChangesAsync();
Success = true;
return Page();
}
}
public class ChangePasswordInput
{
[Required] public string CurrentPassword { get; set; } = string.Empty;
[Required, MinLength(6)] public string NewPassword { get; set; } = string.Empty;
[Required] public string ConfirmPassword { get; set; } = string.Empty;
}
+75
View File
@@ -0,0 +1,75 @@
@page
@model AsadiTools.Pages.Admin.AdminIndexModel
@{ ViewData["Title"] = "داشبورد"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">داشبورد</h1>
<!-- Stats -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
<div class="bg-white rounded-2xl border border-gray-100 p-5">
<div class="w-10 h-10 rounded-xl bg-blue-100 text-blue-600 flex items-center justify-center text-xl mb-3">🛍️</div>
<div class="text-2xl font-extrabold text-gray-900 mb-0.5">@Model.TotalOrders</div>
<div class="text-sm text-gray-500">کل سفارش‌ها</div>
</div>
<div class="bg-white rounded-2xl border border-gray-100 p-5">
<div class="w-10 h-10 rounded-xl bg-yellow-100 text-yellow-600 flex items-center justify-center text-xl mb-3">⏳</div>
<div class="text-2xl font-extrabold text-gray-900 mb-0.5">@Model.PendingOrders</div>
<div class="text-sm text-gray-500">در انتظار تأیید</div>
</div>
<div class="bg-white rounded-2xl border border-gray-100 p-5">
<div class="w-10 h-10 rounded-xl bg-green-100 text-green-600 flex items-center justify-center text-xl mb-3">💰</div>
<div class="text-lg font-extrabold text-gray-900 mb-0.5">@SiteData.FormatPrice(Model.TotalRevenue)</div>
<div class="text-sm text-gray-500">درآمد کل</div>
</div>
<div class="bg-white rounded-2xl border border-gray-100 p-5">
<div class="w-10 h-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center text-xl mb-3">📦</div>
<div class="text-2xl font-extrabold text-gray-900 mb-0.5">@Model.ActiveProducts</div>
<div class="text-sm text-gray-500">محصولات فعال</div>
</div>
</div>
<!-- Recent orders -->
<div class="bg-white rounded-2xl border border-gray-100 p-6">
<div class="flex items-center justify-between mb-5">
<h2 class="font-bold text-gray-900">آخرین سفارش‌ها</h2>
<a href="/Admin/Orders" class="text-sm text-blue-600 hover:underline">مشاهده همه</a>
</div>
@if (!Model.RecentOrders.Any())
{
<p class="text-gray-400 text-sm text-center py-8">هنوز سفارشی ثبت نشده</p>
}
else
{
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b text-gray-500">
<th class="pb-3 text-right font-medium">شماره</th>
<th class="pb-3 text-right font-medium">مشتری</th>
<th class="pb-3 text-right font-medium">مبلغ</th>
<th class="pb-3 text-right font-medium">وضعیت</th>
<th class="pb-3 text-right font-medium">تاریخ</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@foreach (var o in Model.RecentOrders)
{
<tr class="hover:bg-gray-50">
<td class="py-3 font-mono text-xs text-gray-500">@o.OrderNumber</td>
<td class="py-3 font-medium">@o.CustomerName</td>
<td class="py-3 text-blue-700 font-bold">@SiteData.FormatPrice(o.Total)</td>
<td class="py-3">
<span class="text-xs px-2 py-1 rounded-full font-medium @SiteData.OrderStatusBadge(o.Status)">
@SiteData.OrderStatusLabel(o.Status)
</span>
</td>
<td class="py-3 text-gray-400 text-xs">@SiteData.ToJalali(o.CreatedAt)</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
+28
View File
@@ -0,0 +1,28 @@
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;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class AdminIndexModel(AppDbContext db) : PageModel
{
public int TotalOrders { get; private set; }
public int PendingOrders { get; private set; }
public decimal TotalRevenue { get; private set; }
public int ActiveProducts { get; private set; }
public List<Order> RecentOrders { get; private set; } = [];
public async Task<IActionResult> OnGetAsync()
{
TotalOrders = await db.Orders.CountAsync();
PendingOrders = await db.Orders.CountAsync(o => o.Status == OrderStatus.Pending);
TotalRevenue = await db.Orders.Where(o => o.Status != OrderStatus.Cancelled).SumAsync(o => o.Total);
ActiveProducts = await db.Products.CountAsync(p => p.IsActive);
RecentOrders = await db.Orders.OrderByDescending(o => o.Id).Take(6).ToListAsync();
return Page();
}
}
+47
View File
@@ -0,0 +1,47 @@
@page
@model AsadiTools.Pages.Admin.LoginModel
@{ ViewData["Title"] = "ورود به پنل مدیریت"; Layout = null; }
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ورود | پنل مدیریت آساد ابزار</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2") format("woff2"); font-display:swap; }
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Bold.woff2") format("woff2"); font-weight:700; font-display:swap; }
* { font-family:"Vazirmatn",Tahoma,sans-serif !important; }
</style>
</head>
<body class="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div class="bg-blue-700 text-white rounded-2xl p-4 inline-flex text-2xl mb-4">🔧</div>
<h1 class="text-2xl font-extrabold text-gray-900">پنل مدیریت</h1>
<p class="text-gray-500 text-sm mt-1">آساد ابزار کرج</p>
</div>
<form method="post" class="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 space-y-5">
@Html.AntiForgeryToken()
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">نام کاربری</label>
<input asp-for="Input.Username" dir="ltr" placeholder="admin"
class="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">رمز عبور</label>
<input asp-for="Input.Password" type="password" dir="ltr" placeholder="••••••••"
class="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3 rounded-xl">@Model.ErrorMessage</div>
}
<button type="submit" class="w-full bg-blue-700 text-white py-3.5 rounded-xl font-bold hover:bg-blue-800 transition-colors">
🔒 ورود
</button>
<p class="text-xs text-gray-400 text-center">رمز پیش‌فرض: admin / admin1234</p>
</form>
</div>
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
using AsadiTools.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
namespace AsadiTools.Pages.Admin;
public class LoginModel(AppDbContext db) : PageModel
{
[BindProperty]
public InputModel Input { get; set; } = new();
public string? ErrorMessage { get; private set; }
public class InputModel
{
[Required] public string Username { get; set; } = string.Empty;
[Required] public string Password { get; set; } = string.Empty;
}
public IActionResult OnGet()
{
if (User.Identity?.IsAuthenticated == true)
return RedirectToPage("/Admin/Index");
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
var user = db.AdminUsers.FirstOrDefault(u => u.Username == Input.Username);
if (user is null || !BCrypt.Net.BCrypt.Verify(Input.Password, user.PasswordHash))
{
ErrorMessage = "نام کاربری یا رمز اشتباه است";
return Page();
}
var claims = new List<Claim>
{
new(ClaimTypes.Name, user.Username),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
};
var identity = new ClaimsIdentity(claims, "AdminCookie");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("AdminCookie", principal);
return RedirectToPage("/Admin/Index");
}
}
+2
View File
@@ -0,0 +1,2 @@
@page
@model AsadiTools.Pages.Admin.LogoutModel
+14
View File
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Admin;
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnPostAsync()
{
await HttpContext.SignOutAsync("AdminCookie");
return RedirectToPage("/Admin/Login");
}
}
+74
View File
@@ -0,0 +1,74 @@
@page
@model AsadiTools.Pages.Admin.Orders.OrdersIndexModel
@{ ViewData["Title"] = "سفارش‌ها"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">سفارش‌ها</h1>
@if (!Model.Orders.Any())
{
<div class="text-center py-20 text-gray-400">
<div class="text-5xl mb-4">📦</div>
<p>هنوز سفارشی ثبت نشده</p>
</div>
}
else
{
<div class="space-y-4">
@foreach (var o in Model.Orders)
{
<div class="bg-white rounded-2xl border border-gray-100 p-6">
<div class="flex flex-wrap items-start justify-between gap-4 mb-4">
<div>
<div class="flex items-center gap-3 mb-1">
<span class="font-mono text-sm text-gray-500">@o.OrderNumber</span>
<span class="text-xs px-2 py-0.5 rounded-full font-medium @SiteData.OrderStatusBadge(o.Status)">
@SiteData.OrderStatusLabel(o.Status)
</span>
</div>
<div class="font-bold text-gray-900">@o.CustomerName</div>
<div class="text-sm text-gray-500">@o.CustomerPhone</div>
@if (o.CustomerAddress != null) { <div class="text-xs text-gray-400 mt-1">@o.CustomerAddress</div> }
</div>
<div class="text-left">
<div class="text-xl font-extrabold text-blue-700">@SiteData.FormatPrice(o.Total)</div>
<div class="text-xs text-gray-400 mt-1">@SiteData.ToJalaliWithTime(o.CreatedAt)</div>
</div>
</div>
<!-- Items -->
<div class="bg-gray-50 rounded-xl p-4 mb-4">
<p class="text-xs text-gray-500 font-bold mb-2">اقلام سفارش:</p>
<ul class="space-y-1">
@foreach (var item in o.Items)
{
<li class="flex justify-between text-sm">
<span class="text-gray-700">@item.ProductNameFa × @item.Quantity</span>
<span class="text-gray-500">@SiteData.FormatPrice(item.Subtotal)</span>
</li>
}
</ul>
</div>
@if (o.Notes != null) { <p class="text-sm text-gray-500 mb-4">یادداشت: @o.Notes</p> }
<!-- Status update -->
<form method="post" asp-page-handler="UpdateStatus" class="flex items-center gap-3">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@o.Id" />
<span class="text-sm text-gray-500 font-medium">وضعیت:</span>
<select name="status" class="border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
@foreach (var s in Enum.GetValues<AsadiTools.Models.OrderStatus>())
{
<option value="@s" selected="@(o.Status == s)">@SiteData.OrderStatusLabel(s)</option>
}
</select>
<button type="submit" class="bg-blue-700 text-white px-4 py-1.5 rounded-lg text-sm font-medium hover:bg-blue-800 transition-colors">
ذخیره
</button>
</form>
</div>
}
</div>
}
</div>
+31
View File
@@ -0,0 +1,31 @@
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.Orders;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class OrdersIndexModel(AppDbContext db) : PageModel
{
public List<Order> Orders { get; private set; } = [];
public async Task OnGetAsync() =>
Orders = await db.Orders
.Include(o => o.Items)
.OrderByDescending(o => o.Id)
.ToListAsync();
public async Task<IActionResult> OnPostUpdateStatusAsync(int id, OrderStatus status)
{
var order = await db.Orders.FindAsync(id);
if (order is not null)
{
order.Status = status;
await db.SaveChangesAsync();
}
return RedirectToPage();
}
}
+15
View File
@@ -0,0 +1,15 @@
@page
@model AsadiTools.Pages.Admin.Products.CreateModel
@{ ViewData["Title"] = "افزودن محصول"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8 max-w-2xl">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">افزودن محصول جدید</h1>
<form method="post" class="bg-white rounded-2xl border border-gray-100 p-6 space-y-5">
@Html.AntiForgeryToken()
@await Html.PartialAsync("_ProductFormFields", Model.Input)
<div class="flex gap-3">
<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/Products" 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>
+52
View File
@@ -0,0 +1,52 @@
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.Products;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class CreateModel(AppDbContext db) : PageModel
{
[BindProperty] public ProductInput Input { get; set; } = new();
public void OnGet() { }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
db.Products.Add(new Product
{
NameFa = Input.NameFa,
NameEn = Input.NameEn,
Description = Input.Description,
Price = Input.Price,
DiscountPrice = Input.DiscountPrice,
Category = Input.Category,
Brand = string.IsNullOrEmpty(Input.Brand) ? null : Input.Brand,
Sku = Input.Sku,
Stock = Input.Stock,
IsActive = Input.IsActive,
ImageUrl = string.IsNullOrWhiteSpace(Input.ImageUrl) ? null : Input.ImageUrl,
});
await db.SaveChangesAsync();
return RedirectToPage("/Admin/Products/Index");
}
}
public class ProductInput
{
[Required] public string NameFa { get; set; } = string.Empty;
public string? NameEn { get; set; }
public string? Description { get; set; }
[Required, Range(1, int.MaxValue)] public decimal Price { get; set; }
public decimal? DiscountPrice { get; set; }
[Required] public string Category { get; set; } = "carbon";
public string? Brand { get; set; }
public string? Sku { get; set; }
public int Stock { get; set; }
public bool IsActive { get; set; } = true;
public string? ImageUrl { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
@page
@model AsadiTools.Pages.Admin.Products.EditModel
@{ ViewData["Title"] = "ویرایش محصول"; Layout = "_AdminLayout"; }
<div class="p-6 md:p-8 max-w-2xl">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">ویرایش محصول</h1>
<form method="post" class="bg-white rounded-2xl border border-gray-100 p-6 space-y-5">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.ProductId" />
@await Html.PartialAsync("_ProductFormFields", Model.Input)
<div class="flex gap-3">
<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/Products" 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.Products;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class EditModel(AppDbContext db) : PageModel
{
[BindProperty] public ProductInput Input { get; set; } = new();
public int ProductId { get; private set; }
public async Task<IActionResult> OnGetAsync(int id)
{
var p = await db.Products.FindAsync(id);
if (p is null) return NotFound();
ProductId = id;
Input = new ProductInput
{
NameFa = p.NameFa,
NameEn = p.NameEn,
Description = p.Description,
Price = p.Price,
DiscountPrice = p.DiscountPrice,
Category = p.Category,
Brand = p.Brand,
Sku = p.Sku,
Stock = p.Stock,
IsActive = p.IsActive,
ImageUrl = p.ImageUrl,
};
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid) { ProductId = id; return Page(); }
var p = await db.Products.FindAsync(id);
if (p is null) return NotFound();
p.NameFa = Input.NameFa;
p.NameEn = Input.NameEn;
p.Description = Input.Description;
p.Price = Input.Price;
p.DiscountPrice = Input.DiscountPrice;
p.Category = Input.Category;
p.Brand = string.IsNullOrEmpty(Input.Brand) ? null : Input.Brand;
p.Sku = Input.Sku;
p.Stock = Input.Stock;
p.IsActive = Input.IsActive;
p.ImageUrl = string.IsNullOrWhiteSpace(Input.ImageUrl) ? null : Input.ImageUrl;
await db.SaveChangesAsync();
return RedirectToPage("/Admin/Products/Index");
}
}
+70
View File
@@ -0,0 +1,70 @@
@page
@model AsadiTools.Pages.Admin.Products.ProductsIndexModel
@{ 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/Products/Create" class="flex items-center gap-2 bg-blue-700 text-white px-4 py-2.5 rounded-xl text-sm font-bold hover:bg-blue-800 transition-colors">
➕ افزودن محصول
</a>
</div>
<div class="bg-white rounded-2xl border border-gray-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">نام</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">دسته</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">برند</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">قیمت</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">موجودی</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">وضعیت</th>
<th class="px-5 py-3.5 text-right font-medium text-gray-500">عملیات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@foreach (var p in Model.Products)
{
var cat = SiteData.Categories.FirstOrDefault(c => c.Id == p.Category);
var brand = SiteData.Brands.FirstOrDefault(b => b.Id == p.Brand);
<tr class="hover:bg-gray-50/50">
<td class="px-5 py-4">
<div class="font-medium text-gray-900">@p.NameFa</div>
@if (p.Sku != null) { <div class="text-xs text-gray-400 font-mono">@p.Sku</div> }
</td>
<td class="px-5 py-4 text-gray-500">@(cat != null ? cat.Icon + " " + cat.NameFa : p.Category)</td>
<td class="px-5 py-4">
@if (brand != null)
{
<span class="text-xs font-bold px-2 py-0.5 rounded-full text-white" style="background-color:@brand.Color">@brand.NameFa</span>
}
else { <span class="text-gray-400"></span> }
</td>
<td class="px-5 py-4 font-bold text-blue-700">@SiteData.FormatPrice(p.Price)</td>
<td class="px-5 py-4">
<span class="font-bold @(p.Stock == 0 ? "text-red-500" : p.Stock < 5 ? "text-yellow-500" : "text-green-600")">@p.Stock</span>
</td>
<td class="px-5 py-4">
<span class="text-xs px-2 py-1 rounded-full font-medium @(p.IsActive ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-500")">
@(p.IsActive ? "فعال" : "غیرفعال")
</span>
</td>
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<a href="/Admin/Products/Edit?id=@p.Id" class="text-blue-600 hover:text-blue-800 text-xs font-medium">ویرایش</a>
<form method="post" asp-page-handler="Delete" onsubmit="return confirm('حذف شود؟')">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@p.Id" />
<button type="submit" class="text-red-400 hover:text-red-600 text-xs font-medium">حذف</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
+24
View File
@@ -0,0 +1,24 @@
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.Products;
[Authorize(AuthenticationSchemes = "AdminCookie")]
public class ProductsIndexModel(AppDbContext db) : PageModel
{
public List<Product> Products { get; private set; } = [];
public async Task OnGetAsync() =>
Products = await db.Products.OrderByDescending(p => p.Id).ToListAsync();
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var p = await db.Products.FindAsync(id);
if (p is not null) { p.IsActive = false; await db.SaveChangesAsync(); }
return RedirectToPage();
}
}
@@ -0,0 +1,74 @@
@model AsadiTools.Pages.Admin.Products.ProductInput
@using AsadiTools.Services
@{
var inputCls = "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="grid grid-cols-2 gap-4">
<div class="col-span-2">
<label class="text-sm font-medium text-gray-700 mb-1.5 block">نام فارسی <span class="text-red-500">*</span></label>
<input asp-for="NameFa" class="@inputCls" placeholder="مثال: کاربن دیوالت DCD776" />
<span asp-validation-for="NameFa" class="text-red-500 text-xs"></span>
</div>
<div class="col-span-2">
<label class="text-sm font-medium text-gray-700 mb-1.5 block">نام انگلیسی</label>
<input asp-for="NameEn" class="@inputCls" dir="ltr" placeholder="e.g. DeWalt DCD776 Carbon Brush" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">کد محصول (SKU)</label>
<input asp-for="Sku" class="@inputCls" dir="ltr" placeholder="DW-CBR-776" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">دسته‌بندی <span class="text-red-500">*</span></label>
<select asp-for="Category" class="@inputCls">
@foreach (var c in SiteData.Categories)
{
<option value="@c.Id">@c.Icon @c.NameFa</option>
}
</select>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">برند</label>
<select asp-for="Brand" class="@inputCls">
<option value="">بدون برند</option>
@foreach (var b in SiteData.Brands)
{
<option value="@b.Id">@b.NameFa</option>
}
</select>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">موجودی</label>
<input asp-for="Stock" type="number" min="0" class="@inputCls" dir="ltr" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">قیمت (تومان) <span class="text-red-500">*</span></label>
<input asp-for="Price" type="number" min="0" class="@inputCls" dir="ltr" />
<span asp-validation-for="Price" class="text-red-500 text-xs"></span>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">قیمت با تخفیف</label>
<input asp-for="DiscountPrice" type="number" min="0" class="@inputCls" dir="ltr" />
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">وضعیت</label>
<select asp-for="IsActive" class="@inputCls">
<option value="true">فعال</option>
<option value="false">غیرفعال</option>
</select>
</div>
<div class="col-span-2">
<label class="text-sm font-medium text-gray-700 mb-1.5 block">توضیحات</label>
<textarea asp-for="Description" rows="3" class="@inputCls resize-none"></textarea>
</div>
<div class="col-span-2">
<label class="text-sm font-medium text-gray-700 mb-1.5 block">آدرس تصویر (URL)</label>
<input asp-for="ImageUrl" class="@inputCls" dir="ltr" placeholder="https://..." />
<p class="text-xs text-gray-400 mt-1">لینک مستقیم به تصویر محصول. در صورت خالی ماندن آیکون دسته‌بندی نمایش داده می‌شود.</p>
@if (!string.IsNullOrEmpty(Model?.ImageUrl))
{
<img src="@Model.ImageUrl" alt="پیش‌نمایش" class="mt-2 h-24 w-24 object-cover rounded-xl border border-gray-200" onerror="this.style.display='none'" />
}
</div>
</div>
+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();
}
}
+333
View File
@@ -0,0 +1,333 @@
@page "/brands/{brand}"
@model AsadiTools.Pages.Brands.BrandDetailModel
@{ Layout = "_Layout"; var b = Model.Brand!; var c = SiteData.Company; }
@section Head {
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@graph": [
{
"@@type": "LocalBusiness",
"name": "آساد ابزار کرج",
"telephone": "+98@c.TelPhone.Substring(1)",
"address": { "@@type": "PostalAddress", "addressLocality": "کرج", "addressRegion": "البرز", "addressCountry": "IR" },
"openingHours": "Mo-Sa 08:00-18:00",
"priceRange": "$$"
},
{
"@@type": "Service",
"name": "تعمیر ابزار @b.NameFa در کرج",
"provider": { "@@type": "LocalBusiness", "name": "آساد ابزار کرج" },
"areaServed": { "@@type": "City", "name": "کرج" },
"description": "@ViewData["Description"]",
"offers": { "@@type": "Offer", "availability": "https://schema.org/InStock" }
},
{
"@@type": "FAQPage",
"mainEntity": [
@for (int i = 0; i < b.Faqs.Length; i++) {
var faq = b.Faqs[i];
<text>{ "@@type": "Question", "name": "@faq.Q.Replace("\"","'")", "acceptedAnswer": { "@@type": "Answer", "text": "@faq.A.Replace("\"","'")" } }@(i < b.Faqs.Length - 1 ? "," : "")</text>
}
]
},
{
"@@type": "BreadcrumbList",
"itemListElement": [
{ "@@type": "ListItem", "position": 1, "name": "خانه", "item": "/" },
{ "@@type": "ListItem", "position": 2, "name": "برندها", "item": "/brands" },
{ "@@type": "ListItem", "position": 3, "name": "تعمیر ابزار @b.NameFa", "item": "/brands/@b.Id" }
]
}
]
}
</script>
}
<!-- ═══════════════════════ HERO ══════════════════════════════════════════ -->
<div class="relative text-white py-16 px-4 overflow-hidden" style="min-height:320px">
<div class="absolute inset-0">
<img src="@b.HeroImage" alt="تعمیر ابزار @b.NameFa در کرج" class="w-full h-full object-cover" />
<div class="absolute inset-0" style="background:linear-gradient(135deg, @b.Color+ee 0%, @b.Color+bb 50%, rgba(0,0,0,0.7) 100%)"></div>
</div>
<div class="relative max-w-6xl mx-auto">
<!-- Breadcrumb -->
<nav class="flex items-center gap-2 text-sm mb-6 opacity-80" style="color:@(b.TextColor == "#fff" ? "white" : "#374151")">
<a href="/" class="hover:opacity-100">خانه</a>
<span>/</span>
<a href="/brands" class="hover:opacity-100">برندها</a>
<span>/</span>
<span class="opacity-100 font-medium">@b.NameFa</span>
</nav>
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-6">
<div class="w-24 h-24 rounded-2xl flex items-center justify-center text-2xl font-extrabold border-2 border-white/40 bg-white/20 backdrop-blur shrink-0 shadow-xl"
style="color:@b.TextColor">
@b.Name.Substring(0, Math.Min(3, b.Name.Length))
</div>
<div>
<div class="flex flex-wrap items-center gap-3 mb-3">
<h1 class="text-4xl font-extrabold" style="color:@(b.TextColor == "#fff" ? "white" : "#111827")">
تعمیر ابزار @b.NameFa در کرج
</h1>
@if (b.IsOfficial)
{
<span class="bg-yellow-400 text-gray-900 text-sm font-bold px-3 py-1 rounded-full shadow-lg">🛡️ نمایندگی رسمی</span>
}
</div>
<p class="text-lg max-w-2xl leading-7 opacity-90" style="color:@(b.TextColor == "#fff" ? "#e5e7eb" : "#374151")">
@b.Tagline
</p>
<div class="flex flex-wrap gap-2 mt-4 text-sm opacity-80" style="color:@(b.TextColor == "#fff" ? "#d1d5db" : "#6b7280")">
<span>📍 تأسیس @b.Founded</span>
<span>·</span>
<span>🌍 @b.Country</span>
@if (b.IsOfficial) { <span>·</span> <span class="text-yellow-300 font-bold">✅ نمایندگی رسمی در کرج</span> }
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ MAIN GRID ═════════════════════════════════════ -->
<div class="max-w-6xl mx-auto px-4 py-12 grid lg:grid-cols-3 gap-10">
<!-- ══ LEFT / MAIN ════════════════════════════════════════════════════ -->
<div class="lg:col-span-2 space-y-12">
@if (b.IsOfficial) {
<!-- Official badge -->
<div class="bg-yellow-50 border-2 border-yellow-300 rounded-2xl p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-yellow-400 rounded-xl flex items-center justify-center text-2xl shrink-0">🛡️</div>
<div>
<h2 class="text-xl font-extrabold text-gray-900 mb-2">نمایندگی رسمی مجاز @b.NameFa در کرج</h2>
<p class="text-gray-600 leading-7">آساد ابزار تنها نمایندگی رسمی و مجاز برند @b.NameFa در شهر کرج و استان البرز است. تمام تعمیرات توسط تکنیسین‌های آموزش‌دیده رسمی و با قطعات کاملاً اورجینال @b.NameFa انجام می‌شود. هر تعمیر دارای ضمانت‌نامه کتبی ۳ ماهه است.</p>
</div>
</div>
</div>
}
<!-- About brand -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2 flex items-center gap-3" style="border-color:@b.Color">
<span class="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold text-white" style="background:@b.Color">@b.Name[0]</span>
درباره برند @b.NameFa
</h2>
<div class="prose prose-lg max-w-none space-y-4 text-gray-600 leading-8">
<p>@b.About1</p>
<p>@b.About2</p>
<p>@b.About3</p>
</div>
</section>
<!-- Popular models -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2" style="border-color:@b.Color">
مدل‌های محبوب @b.NameFa در ایران
</h2>
<div class="grid sm:grid-cols-2 gap-4">
@foreach (var m in b.Models)
{
<div class="bg-white rounded-2xl border border-gray-100 p-5 hover:shadow-md transition-shadow hover:border-gray-200">
<div class="flex items-center justify-between mb-2">
<span class="font-mono text-sm font-bold text-gray-800">@m.Model</span>
<span class="text-xs font-bold px-2 py-0.5 rounded-full text-white" style="background:@b.Color">@m.Watts</span>
</div>
<div class="font-semibold text-gray-900 mb-1">@m.NameFa</div>
<p class="text-sm text-gray-500 leading-6">@m.Desc</p>
</div>
}
</div>
</section>
<!-- Repair services -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2" style="border-color:@b.Color">
خدمات تعمیر @b.NameFa در آساد ابزار
</h2>
<div class="grid sm:grid-cols-2 gap-3">
@foreach (var svc in b.RepairServices)
{
<div class="flex items-start gap-3 bg-white rounded-xl border border-gray-100 p-4 hover:border-gray-200 transition-colors">
<span class="w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0 mt-0.5" style="background:@b.Color">✓</span>
<span class="text-sm text-gray-700 leading-6">@svc</span>
</div>
}
</div>
</section>
<!-- Common problems -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2" style="border-color:@b.Color">
مشکلات رایج ابزار @b.NameFa و راه‌حل
</h2>
<div class="space-y-4">
@foreach (var prob in b.CommonProblems)
{
<div class="bg-white rounded-2xl border border-gray-100 p-6">
<h3 class="font-bold text-gray-900 mb-3 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-red-100 text-red-600 flex items-center justify-center text-xs shrink-0">!</span>
@prob.Problem
</h3>
<div class="grid sm:grid-cols-2 gap-4 text-sm">
<div class="bg-red-50 rounded-xl p-3">
<div class="text-xs font-bold text-red-700 mb-1">علت احتمالی:</div>
<div class="text-gray-600 leading-6">@prob.Cause</div>
</div>
<div class="bg-green-50 rounded-xl p-3">
<div class="text-xs font-bold text-green-700 mb-1">راه‌حل:</div>
<div class="text-gray-600 leading-6">@prob.Solution</div>
</div>
</div>
</div>
}
</div>
</section>
<!-- Repair process -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2" style="border-color:@b.Color">
فرآیند تعمیر ابزار @b.NameFa
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@{ var steps = new[] { ("۱","تحویل ابزار","حضوری یا از طریق پست"), ("۲","بررسی رایگان","تشخیص دقیق عیب"), ("۳","تعمیر تخصصی","قطعه اصل + ضمانت"), ("۴","تحویل با گارانتی","ضمانت‌نامه کتبی ۳ ماهه") }; }
@foreach (var step in steps)
{
<div class="text-center p-5 bg-gray-50 rounded-2xl border border-gray-100 hover:bg-white hover:shadow-sm transition-all">
<div class="w-11 h-11 rounded-full flex items-center justify-center text-lg font-extrabold mx-auto mb-3 shadow"
style="background:@b.Color;color:@b.TextColor">@step.Item1</div>
<div class="font-bold text-gray-800 text-sm mb-1">@step.Item2</div>
<div class="text-xs text-gray-400">@step.Item3</div>
</div>
}
</div>
</section>
<!-- FAQ -->
<section>
<h2 class="text-2xl font-extrabold text-gray-900 mb-6 pb-3 border-b-2" style="border-color:@b.Color">
سوالات متداول درباره تعمیر ابزار @b.NameFa
</h2>
<div class="space-y-3" id="faq-list">
@for (int i = 0; i < b.Faqs.Length; i++)
{
var faq = b.Faqs[i];
<details class="bg-white rounded-2xl border border-gray-100 overflow-hidden group" @(i == 0 ? "open" : "")>
<summary class="flex items-center justify-between p-5 cursor-pointer font-bold text-gray-900 hover:bg-gray-50 select-none list-none">
<span>@faq.Q</span>
<span class="text-gray-400 group-open:rotate-180 transition-transform shrink-0 mr-3">▾</span>
</summary>
<div class="px-5 pb-5 text-gray-600 leading-8 text-sm border-t border-gray-50 pt-4">@faq.A</div>
</details>
}
</div>
</section>
</div>
<!-- ══ SIDEBAR ══════════════════════════════════════════════════════ -->
<div class="space-y-5 lg:sticky lg:top-24 lg:self-start">
<!-- CTA card -->
<div class="rounded-2xl p-6 text-center shadow-lg" style="background:@b.Color">
<div class="text-2xl mb-2">🔧</div>
<h3 class="font-extrabold text-lg mb-1" style="color:@(b.TextColor == "#fff" ? "white" : "#111827")">
درخواست تعمیر @b.NameFa
</h3>
<p class="text-sm mb-5 opacity-80" style="color:@(b.TextColor == "#fff" ? "#e5e7eb" : "#374151")">
مشاوره رایگان – تشخیص رایگان
</p>
<a href="tel:@c.TelPhone"
class="block bg-white font-bold text-center py-3 rounded-xl hover:opacity-90 mb-3 transition-opacity shadow"
style="color:@b.Color">
📞 @c.Phone
</a>
<a href="https://wa.me/@c.Whatsapp" target="_blank"
class="block bg-green-500 text-white font-bold text-center py-3 rounded-xl hover:bg-green-600 transition-colors">
💬 پیام واتساپ
</a>
</div>
<!-- Guarantees -->
<div class="bg-white rounded-2xl border border-gray-100 p-6">
<h3 class="font-bold text-gray-900 mb-4 flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-xs">⭐</span>
مزایای تعمیر در آساد ابزار
</h3>
<ul class="space-y-3 text-sm text-gray-600">
@foreach (var item in new[] {
"تشخیص عیب رایگان بدون پیش‌پرداخت",
"قطعات ۱۰۰٪ اورجینال تأییدشده",
"ضمانت‌نامه کتبی ۳ ماهه",
"تکنیسین مجاز و متخصص",
"قیمت شفاف پیش از شروع",
"تحویل سریع زیر ۴۸ ساعت",
"ارسال از سراسر کشور"
})
{
<li class="flex items-center gap-2">
<span class="text-green-500 shrink-0">✓</span> @item
</li>
}
</ul>
</div>
<!-- Parts shop link -->
<a href="/Shop?brand=@b.Id"
class="block bg-gray-50 border border-gray-200 rounded-2xl p-5 hover:shadow-md hover:bg-white transition-all">
<div class="flex items-center gap-3">
<span class="text-2xl">🛒</span>
<div>
<h3 class="font-bold text-gray-900 text-sm">قطعات یدکی @b.NameFa</h3>
<p class="text-xs text-gray-400 mt-0.5">خرید آنلاین از فروشگاه ما</p>
</div>
</div>
</a>
<!-- Other brands -->
<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 ob in BrandSeoData.AllBrands.Where(x => x.Id != b.Id))
{
<a href="/brands/@ob.Id"
class="text-xs px-3 py-1.5 rounded-full border border-gray-200 text-gray-600 hover:text-white transition-all hover:border-transparent"
style="--hover-bg:@ob.Color"
onmouseover="this.style.background='@ob.Color';this.style.borderColor='@ob.Color'"
onmouseout="this.style.background='';this.style.borderColor=''">
@ob.NameFa
</a>
}
</div>
</div>
</div>
</div>
<!-- ══════════════════════ BOTTOM CTA ════════════════════════════════════ -->
<section class="py-14 px-4" style="background:@b.Color">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-2xl font-extrabold mb-3" style="color:@(b.TextColor == "#fff" ? "white" : "#111827")">
ابزار @b.NameFa شما خراب شده؟
</h2>
<p class="mb-8 text-lg opacity-85" style="color:@(b.TextColor == "#fff" ? "#e5e7eb" : "#374151")">
همین الان تماس بگیرید. تشخیص رایگان، تعمیر با ضمانت کتبی ۳ ماهه.
</p>
<div class="flex flex-wrap justify-center gap-4">
<a href="tel:@c.TelPhone"
class="bg-white font-bold px-8 py-3.5 rounded-xl hover:opacity-90 transition-opacity text-lg shadow-lg"
style="color:@b.Color">
📞 @c.Phone
</a>
<a href="https://wa.me/@c.Whatsapp" target="_blank"
class="bg-green-600 text-white font-bold px-8 py-3.5 rounded-xl hover:bg-green-700 transition-colors text-lg shadow-lg">
💬 واتساپ
</a>
<a href="/Shop?brand=@b.Id"
class="bg-white/20 border border-white/50 font-bold px-8 py-3.5 rounded-xl hover:bg-white/30 transition-colors text-lg"
style="color:@(b.TextColor == "#fff" ? "white" : "#111827")">
🛒 قطعات یدکی
</a>
</div>
</div>
</section>
+22
View File
@@ -0,0 +1,22 @@
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Brands;
public class BrandDetailModel : PageModel
{
public BrandSeoPage? Brand { get; private set; }
public IActionResult OnGet(string brand)
{
Brand = BrandSeoData.AllBrands.FirstOrDefault(b =>
b.Id.Equals(brand, StringComparison.OrdinalIgnoreCase));
if (Brand is null) return NotFound();
ViewData["Title"] = $"تعمیر ابزار {Brand.NameFa} در کرج | آساد ابزار{(Brand.IsOfficial ? " نمایندگی رسمی" : "")}";
ViewData["Description"] = $"تعمیر تخصصی ابزار {Brand.NameFa} در کرج با ضمانت ۳ ماهه. قطعات اصل، تکنیسین مجاز. {string.Join("، ", Brand.RepairServices.Take(3))}. تماس: ۰۲۶-۳۴۵۶۷۸۹۰";
return Page();
}
}
+63
View File
@@ -0,0 +1,63 @@
@page
@model AsadiTools.Pages.Brands.BrandsIndexModel
@{ 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-12">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
@foreach (var b in Model.Brands)
{
<a href="/brands/@b.Id"
class="group relative bg-white rounded-2xl overflow-hidden border-2 hover:shadow-xl transition-all hover:-translate-y-1"
style="border-color:@(b.IsOfficial ? "#f59e0b" : "#e5e7eb")">
@if (b.IsOfficial)
{
<div class="absolute top-3 right-3 z-10 bg-yellow-400 text-gray-900 text-xs font-bold px-2.5 py-1 rounded-full shadow">🛡️ رسمی</div>
}
<div class="relative h-36 overflow-hidden">
<img src="@b.HeroImage" alt="تعمیر ابزار @b.NameFa" loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
<div class="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
<div class="absolute bottom-3 right-4 flex items-center gap-2">
<div class="w-9 h-9 rounded-lg flex items-center justify-center text-xs font-extrabold shadow"
style="background-color:@b.Color;color:@b.TextColor">@b.Name.Substring(0,2)</div>
<span class="text-white font-bold text-lg">@b.NameFa</span>
</div>
</div>
<div class="p-5">
<p class="text-sm text-gray-500 leading-6 mb-3 line-clamp-2">@b.Tagline</p>
<div class="flex items-center justify-between text-xs text-gray-400">
<span>📍 @b.Country · @b.Founded</span>
<span class="text-blue-600 font-medium group-hover:underline">جزئیات </span>
</div>
</div>
</a>
}
</div>
<!-- Why trust us -->
<div class="mt-16 bg-blue-900 rounded-3xl p-10 text-white text-center">
<h2 class="text-2xl font-extrabold mb-3">چرا آساد ابزار؟</h2>
<p class="text-blue-200 mb-8 max-w-2xl mx-auto">بیش از ۱۵ سال تجربه تعمیر ابزارهای برقی صنعتی، نمایندگی رسمی دیوالت در کرج، با قطعات اصل و ضمانت کتبی</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@foreach (var s in new[] { ("+۱۵","سال تجربه","🏆"), ("+۵۰۰۰","دستگاه تعمیر شده","🔧"), ("۸","برند پشتیبانی","⭐"), ("۱۰۰٪","ضمانت تعمیر","🛡️") })
{
<div class="bg-white/10 rounded-2xl p-5">
<div class="text-2xl mb-1">@s.Item3</div>
<div class="text-2xl font-extrabold text-yellow-300">@s.Item1</div>
<div class="text-xs text-blue-200 mt-0.5">@s.Item2</div>
</div>
}
</div>
</div>
</div>
+15
View File
@@ -0,0 +1,15 @@
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Brands;
public class BrandsIndexModel : PageModel
{
public BrandSeoPage[] Brands { get; } = BrandSeoData.AllBrands;
public void OnGet()
{
ViewData["Title"] = "تعمیر تخصصی ابزار برقی در کرج | همه برندها | آساد ابزار";
ViewData["Description"] = "تعمیر ابزار دیوالت، بوش، هیلتی، متابو، رونیکس، توسن، ریوبی و آ.ا.گ در کرج. نمایندگی رسمی دیوالت. قطعات اصل، ضمانت ۳ ماهه.";
}
}
+85
View File
@@ -0,0 +1,85 @@
@page
@model AsadiTools.Pages.Cart.CartIndexModel
@{ ViewData["Title"] = "سبد خرید"; Layout = "_Layout"; }
@if (!Model.Items.Any())
{
<div class="min-h-[60vh] flex items-center justify-center">
<div class="text-center py-20">
<div class="text-6xl mb-4">🛒</div>
<h2 class="text-xl font-bold text-gray-600 mb-2">سبد خرید شما خالی است</h2>
<p class="text-gray-400 mb-6">قطعات مورد نیاز خود را از فروشگاه انتخاب کنید</p>
<a href="/Shop" class="bg-blue-700 text-white px-6 py-3 rounded-xl font-bold hover:bg-blue-800 transition-colors">مشاهده فروشگاه</a>
</div>
</div>
}
else
{
<div class="max-w-4xl mx-auto px-4 py-10">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">سبد خرید</h1>
<div class="grid md:grid-cols-3 gap-6">
<!-- Items -->
<div class="md:col-span-2 space-y-3">
@foreach (var item in Model.Items)
{
<div class="bg-white rounded-2xl border border-gray-100 p-5 flex items-center gap-4">
<div class="bg-blue-50 rounded-xl w-14 h-14 flex items-center justify-center text-2xl shrink-0">🔧</div>
<div class="flex-1 min-w-0">
<h3 class="font-bold text-gray-900 text-sm">@item.NameFa</h3>
@if (item.Sku != null) { <p class="text-xs text-gray-400">کد: @item.Sku</p> }
<p class="text-blue-700 font-bold mt-1">@SiteData.FormatPrice(item.Price)</p>
</div>
<!-- Qty -->
<div class="flex items-center gap-2 shrink-0">
<form method="post" asp-page-handler="UpdateQty">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@item.ProductId" />
<input type="hidden" name="qty" value="@(item.Qty - 1)" />
<button type="submit" class="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 text-lg"></button>
</form>
<span class="w-8 text-center font-bold text-sm">@item.Qty</span>
<form method="post" asp-page-handler="UpdateQty">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@item.ProductId" />
<input type="hidden" name="qty" value="@(item.Qty + 1)" />
<button type="submit" class="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 text-lg">+</button>
</form>
</div>
<div class="text-left shrink-0">
<p class="font-bold text-gray-800 text-sm">@SiteData.FormatPrice(item.Subtotal)</p>
<form method="post" asp-page-handler="Remove" class="mt-1">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@item.ProductId" />
<button type="submit" class="text-red-400 hover:text-red-600 text-xs">حذف</button>
</form>
</div>
</div>
}
<form method="post" asp-page-handler="Clear" class="mt-2">
@Html.AntiForgeryToken()
<button type="submit" class="text-sm text-red-400 hover:text-red-600 transition-colors">🗑️ پاک کردن سبد خرید</button>
</form>
</div>
<!-- Summary -->
<div>
<div class="bg-white rounded-2xl border border-gray-100 p-6 sticky top-24">
<h2 class="font-bold text-gray-900 mb-4 pb-3 border-b">خلاصه سفارش</h2>
<div class="space-y-2 text-sm mb-4">
<div class="flex justify-between text-gray-600">
<span>تعداد اقلام:</span>
<span>@Model.Items.Sum(i => i.Qty) عدد</span>
</div>
<div class="flex justify-between font-bold text-gray-900 text-base pt-2 border-t">
<span>جمع کل:</span>
<span class="text-blue-700">@SiteData.FormatPrice(Model.Total)</span>
</div>
</div>
<a href="/Checkout" class="block bg-blue-700 text-white text-center py-3.5 rounded-xl font-bold hover:bg-blue-800 transition-colors mb-3">ادامه و ثبت سفارش</a>
<a href="/Shop" class="block text-sm text-blue-600 text-center hover:underline">← ادامه خرید</a>
</div>
</div>
</div>
</div>
}
+36
View File
@@ -0,0 +1,36 @@
using AsadiTools.Models;
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Cart;
public class CartIndexModel(CartService cart) : PageModel
{
public List<CartItem> Items { get; private set; } = [];
public decimal Total { get; private set; }
public void OnGet()
{
Items = cart.GetItems();
Total = cart.Total;
}
public IActionResult OnPostUpdateQty(int productId, int qty)
{
cart.UpdateQty(productId, qty);
return RedirectToPage();
}
public IActionResult OnPostRemove(int productId)
{
cart.RemoveItem(productId);
return RedirectToPage();
}
public IActionResult OnPostClear()
{
cart.Clear();
return RedirectToPage();
}
}
+90
View File
@@ -0,0 +1,90 @@
@page
@model AsadiTools.Pages.Checkout.CheckoutIndexModel
@{ ViewData["Title"] = "ثبت سفارش"; Layout = "_Layout"; }
@if (Model.OrderNumber != null)
{
<div class="min-h-[60vh] flex items-center justify-center">
<div class="text-center max-w-md mx-auto px-4 py-20">
<div class="text-6xl mb-4">✅</div>
<h2 class="text-2xl font-extrabold text-gray-900 mb-3">سفارش ثبت شد!</h2>
<p class="text-gray-600 mb-2">شماره سفارش شما:</p>
<p class="text-xl font-bold text-blue-700 mb-6 font-mono">@Model.OrderNumber</p>
<p class="text-gray-500 text-sm mb-8">به زودی با شما تماس می‌گیریم تا جزئیات سفارش را هماهنگ کنیم.</p>
<a href="/" class="bg-blue-700 text-white px-6 py-3 rounded-xl font-bold hover:bg-blue-800 transition-colors">بازگشت به خانه</a>
</div>
</div>
}
else
{
<div class="max-w-4xl mx-auto px-4 py-10">
<h1 class="text-2xl font-extrabold text-gray-900 mb-8">ثبت سفارش</h1>
<div class="grid md:grid-cols-3 gap-6">
<form method="post" class="md:col-span-2">
@Html.AntiForgeryToken()
<div class="bg-white rounded-2xl border border-gray-100 p-6 space-y-4">
<h2 class="font-bold text-gray-900 mb-2">اطلاعات خریدار</h2>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">نام و نام خانوادگی <span class="text-red-500">*</span></label>
<input asp-for="Input.CustomerName" required
class="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"
placeholder="مثال: علی احمدی" />
<span asp-validation-for="Input.CustomerName" class="text-red-500 text-xs"></span>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">شماره موبایل <span class="text-red-500">*</span></label>
<input asp-for="Input.CustomerPhone" required dir="ltr"
class="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"
placeholder="09xxxxxxxxx" />
<span asp-validation-for="Input.CustomerPhone" class="text-red-500 text-xs"></span>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">آدرس (برای ارسال)</label>
<textarea asp-for="Input.CustomerAddress" rows="3"
class="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 resize-none"
placeholder="کرج، ..."></textarea>
</div>
<div>
<label class="text-sm font-medium text-gray-700 mb-1.5 block">توضیحات</label>
<textarea asp-for="Input.Notes" rows="2"
class="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 resize-none"
placeholder="توضیحات اضافی..."></textarea>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3 rounded-xl">@Model.ErrorMessage</div>
}
<button type="submit" class="w-full bg-blue-700 text-white py-4 rounded-xl font-bold text-lg hover:bg-blue-800 transition-colors">
ثبت نهایی سفارش
</button>
</div>
</form>
<!-- Summary -->
<div>
<div class="bg-white rounded-2xl border border-gray-100 p-6 sticky top-24">
<h2 class="font-bold text-gray-900 mb-4 pb-3 border-b">اقلام سفارش</h2>
<div class="space-y-3 mb-4">
@foreach (var item in Model.CartItems)
{
<div class="flex justify-between text-sm">
<span class="text-gray-600 truncate ml-2">@item.NameFa × @item.Qty</span>
<span class="font-bold shrink-0">@SiteData.FormatPrice(item.Subtotal)</span>
</div>
}
</div>
<div class="border-t pt-3 flex justify-between font-bold">
<span>جمع کل:</span>
<span class="text-blue-700">@SiteData.FormatPrice(Model.CartItems.Sum(i => i.Subtotal))</span>
</div>
</div>
</div>
</div>
</div>
}
+71
View File
@@ -0,0 +1,71 @@
using AsadiTools.Data;
using AsadiTools.Models;
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace AsadiTools.Pages.Checkout;
public class CheckoutIndexModel(AppDbContext db, CartService cart) : PageModel
{
[BindProperty]
public InputModel Input { get; set; } = new();
public List<CartItem> CartItems { get; private set; } = [];
public string? OrderNumber { get; private set; }
public string? ErrorMessage { get; private set; }
public class InputModel
{
[Required(ErrorMessage = "نام الزامی است")]
public string CustomerName { get; set; } = string.Empty;
[Required(ErrorMessage = "شماره موبایل الزامی است")]
public string CustomerPhone { get; set; } = string.Empty;
public string? CustomerAddress { get; set; }
public string? Notes { get; set; }
}
public IActionResult OnGet()
{
CartItems = cart.GetItems();
if (!CartItems.Any()) return RedirectToPage("/Shop/Index");
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
CartItems = cart.GetItems();
if (!CartItems.Any()) return RedirectToPage("/Shop/Index");
if (!ModelState.IsValid) return Page();
var orderNumber = $"ORD-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
var order = new Order
{
OrderNumber = orderNumber,
CustomerName = Input.CustomerName,
CustomerPhone = Input.CustomerPhone,
CustomerAddress = Input.CustomerAddress,
Notes = Input.Notes,
Items = CartItems.Select(i => new OrderItem
{
ProductId = i.ProductId,
ProductNameFa = i.NameFa,
Price = i.Price,
Quantity = i.Qty,
}).ToList(),
};
order.Subtotal = order.Items.Sum(i => i.Subtotal);
order.Total = order.Subtotal;
db.Orders.Add(order);
await db.SaveChangesAsync();
cart.Clear();
OrderNumber = orderNumber;
return Page();
}
}
+96
View File
@@ -0,0 +1,96 @@
@page
@model AsadiTools.Pages.Contact.ContactIndexModel
@{
ViewData["Title"] = "تماس با آساد ابزار – نمایندگی دیوالت کرج";
ViewData["Description"] = "آدرس، تلفن و ساعات کاری آساد ابزار کرج. نمایندگی رسمی دیوالت در کرج.";
Layout = "_Layout";
var c = SiteData.Company;
var mapBbox = $"{c.MapLng - 0.02},{c.MapLat - 0.02},{c.MapLng + 0.02},{c.MapLat + 0.02}";
var mapUrl = $"https://www.openstreetmap.org/export/embed.html?bbox={mapBbox}&layer=mapnik&marker={c.MapLat},{c.MapLng}";
}
<div class="bg-blue-800 text-white py-12 px-4">
<div class="max-w-6xl mx-auto">
<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-12">
<div class="grid md:grid-cols-2 gap-10">
<div>
<h2 class="text-xl font-bold text-gray-900 mb-6">اطلاعات تماس</h2>
<div class="space-y-4">
<div class="flex items-start gap-4 p-5 bg-white rounded-2xl border border-gray-100">
<span class="text-2xl mt-0.5">📞</span>
<div>
<div class="font-bold text-gray-800 mb-2">تلفن</div>
<a href="tel:@c.TelPhone" class="block text-blue-600 hover:underline text-lg font-bold">@c.Phone</a>
<a href="tel:@c.TelMobile" class="block text-blue-600 hover:underline">@c.Mobile</a>
</div>
</div>
<div class="flex items-start gap-4 p-5 bg-white rounded-2xl border border-gray-100">
<span class="text-2xl mt-0.5">📍</span>
<div>
<div class="font-bold text-gray-800 mb-1">آدرس</div>
<p class="text-gray-600">@c.Address</p>
</div>
</div>
<div class="flex items-start gap-4 p-5 bg-white rounded-2xl border border-gray-100">
<span class="text-2xl mt-0.5">🕐</span>
<div>
<div class="font-bold text-gray-800 mb-1">ساعات کاری</div>
<p class="text-gray-600">@c.WorkingHours</p>
</div>
</div>
<div class="flex gap-3">
<a href="https://wa.me/@c.Whatsapp" target="_blank"
class="flex-1 flex items-center justify-center gap-2 bg-green-500 text-white py-3.5 rounded-xl font-bold hover:bg-green-600 transition-colors">
💬 واتساپ
</a>
<a href="https://instagram.com/@c.Instagram" target="_blank"
class="flex-1 flex items-center justify-center gap-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white py-3.5 rounded-xl font-bold hover:opacity-90 transition-opacity">
📷 اینستاگرام
</a>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">برندهای رسمی</h2>
<div class="grid grid-cols-2 gap-3">
@foreach (var b in SiteData.Brands)
{
<div class="flex items-center gap-3 p-3 bg-white rounded-xl border border-gray-100">
<div class="w-9 h-9 rounded-lg flex items-center justify-center text-sm font-bold shrink-0"
style="background-color:@b.Color;color:@b.TextColor">@b.Name[0]</div>
<div>
<div class="font-bold text-gray-800 text-sm">@b.NameFa</div>
@if (b.IsOfficial) { <div class="text-xs text-yellow-600 font-medium">نمایندگی رسمی</div> }
</div>
</div>
}
</div>
</div>
</div>
<div>
<h2 class="text-xl font-bold text-gray-900 mb-6">موقعیت روی نقشه</h2>
<div class="rounded-2xl overflow-hidden border border-gray-200 mb-6" style="height:320px">
<iframe src="@mapUrl"
width="100%" height="320" style="border:0" loading="lazy"
title="موقعیت آساد ابزار کرج در نقشه"></iframe>
</div>
<p class="text-xs text-gray-400 mb-4">
نقشه از <a href="https://www.openstreetmap.org" target="_blank" class="underline">OpenStreetMap</a> —
پس از تعیین آدرس دقیق، مختصات را در <code>SiteData.Company.MapLat/MapLng</code> به‌روز کنید.
</p>
<div class="bg-yellow-50 border border-yellow-200 rounded-2xl p-5">
<h3 class="font-bold text-gray-900 mb-2">مراجعه حضوری</h3>
<p class="text-sm text-gray-600 leading-7">
برای تعمیر ابزار می‌توانید حضوراً مراجعه کنید یا با تماس قبلی ابزار خود را برای ما ارسال نمایید.
پس از تشخیص عیب، هزینه تعمیر اعلام می‌شود.
</p>
</div>
</div>
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Contact;
public class ContactIndexModel : PageModel
{
public void OnGet() { }
}
+290
View File
@@ -0,0 +1,290 @@
@page
@model AsadiTools.Pages.DeWalt.DeWaltIndexModel
@{ Layout = "_Layout"; }
@section Head {
<script type="application/ld+json">
{"@@context":"https://schema.org","@@type":"Service","name":"تعمیر ابزار دیوالت در کرج","provider":{"@@type":"LocalBusiness","name":"آساد ابزار کرج","telephone":"+98-261-XXXXXXX","address":{"@@type":"PostalAddress","addressLocality":"کرج","addressCountry":"IR"}},"serviceType":"تعمیر ابزار برقی","brand":{"@@type":"Brand","name":"DeWalt"},"areaServed":"کرج"}
</script>
}
<!-- ═══════════ HERO ═══════════ -->
<section class="relative text-white overflow-hidden" style="min-height:420px;background:#1a1200">
<div class="absolute inset-0 bg-cover bg-center opacity-30"
style="background-image:url('https://images.unsplash.com/photo-1504148455328-c376907d081c?w=1400&q=80&auto=format&fit=crop')"></div>
<div class="absolute inset-0 bg-gradient-to-l from-yellow-900/40 to-transparent"></div>
<div class="relative max-w-6xl mx-auto px-4 py-16 flex flex-col md:flex-row items-center gap-10">
<!-- DeWalt logo area -->
<div class="shrink-0">
<div class="w-40 h-40 rounded-3xl flex items-center justify-center shadow-2xl border-4 border-yellow-400"
style="background:#FFCD00">
<span class="font-extrabold text-gray-900 text-3xl tracking-tighter">DeWALT</span>
</div>
</div>
<div>
<div class="inline-flex items-center gap-2 bg-yellow-400 text-gray-900 text-sm font-bold px-4 py-1.5 rounded-full mb-4">
🛡️ نمایندگی رسمی DeWalt در کرج
</div>
<h1 class="text-4xl md:text-5xl font-extrabold leading-tight mb-4">
تعمیر تخصصی<br>
<span class="text-yellow-400">ابزار دیوالت</span>
</h1>
<p class="text-gray-300 text-lg leading-8 max-w-2xl mb-6">
آساد ابزار مجاز تنها مرکز تعمیر رسمی ابزار DeWalt در شهر کرج است.
بیش از ۱۵ مدل از پرفروش‌ترین ابزار دیوالت را با قطعات اصل تعمیر می‌کنیم.
</p>
<div class="flex flex-wrap gap-3">
<a href="tel:02634567890" class="bg-yellow-400 text-gray-900 font-bold px-6 py-3 rounded-xl hover:bg-yellow-300 transition-colors">📞 تماس برای تعمیر</a>
<a href="#tools" class="border-2 border-yellow-400/50 text-yellow-300 font-bold px-6 py-3 rounded-xl hover:border-yellow-400 transition-colors">مشاهده ابزارها ↓</a>
</div>
</div>
</div>
</section>
<!-- ═══════════ TRUST STRIP ═══════════ -->
<div class="bg-yellow-400 py-3 px-4">
<div class="max-w-6xl mx-auto flex flex-wrap justify-center gap-6 text-gray-900 text-sm font-semibold">
@foreach (var t in new[] { "🛡️ ضمانت ۳ ماهه کتبی", "🔩 قطعات اصل DeWalt", "⚡ تحویل زیر ۴۸ ساعت", "📞 مشاوره رایگان", "🎓 تکنیسین آموزش‌دیده DeWalt" })
{
<span>@t</span>
}
</div>
</div>
<!-- ═══════════ CATEGORY TABS ═══════════ -->
<nav id="tools" class="bg-white border-b sticky top-[65px] z-40">
<div class="max-w-6xl mx-auto px-4 flex gap-1 overflow-x-auto py-2">
@{
var cats = new[] {
("all", "همه ابزار"),
("drill", "🔩 دریل"),
("driver", "🔧 پیچ‌گوشتی"),
("grinder", "⚙️ فرز آنگولر"),
("rotary-hammer", "🏗️ بتن‌کن"),
("saw", "🪚 ابزار برش"),
("chop-saw", "💿 گردبر"),
("miter-saw", "📐 فارسی‌بر"),
("woodworking", "🪵 نجاری"),
("multi", "🔧 چندکاره"),
("sander", "💨 سنباده"),
("hedge-trimmer", "🌿 شمشادزن"),
("laser-level", "🔴 تراز لیزری"),
("laser-measure", "📏 متر لیزری"),
};
}
@foreach (var (catId, catName) in cats)
{
<button onclick="filterTools('@catId')"
id="tab-@catId"
class="tab-btn shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors text-gray-600 hover:bg-yellow-50 hover:text-yellow-700 whitespace-nowrap">
@catName
</button>
}
</div>
</nav>
<!-- ═══════════ TOOLS GRID ═══════════ -->
<section class="py-12 px-4 bg-gray-50">
<div class="max-w-6xl mx-auto">
<div id="tools-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@{
var catImages = new Dictionary<string, string>
{
["drill"] = "https://images.unsplash.com/photo-1572981779307-38b8cabb2407?w=500&q=75&auto=format&fit=crop",
["driver"] = "https://images.unsplash.com/photo-1530124566582-a618bc2615dc?w=500&q=75&auto=format&fit=crop",
["grinder"] = "https://images.unsplash.com/photo-1487452066049-a710f7296400?w=500&q=75&auto=format&fit=crop",
["rotary-hammer"] = "https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=500&q=75&auto=format&fit=crop",
["saw"] = "https://images.unsplash.com/photo-1504148455328-c376907d081c?w=500&q=75&auto=format&fit=crop",
["chop-saw"] = "https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=500&q=75&auto=format&fit=crop",
["miter-saw"] = "https://images.unsplash.com/photo-1558618047-3c8c1d8b9df1?w=500&q=75&auto=format&fit=crop",
["woodworking"] = "https://images.unsplash.com/photo-1588854337115-1c67d9247e4d?w=500&q=75&auto=format&fit=crop",
["multi"] = "https://images.unsplash.com/photo-1518770660439-4636190af475?w=500&q=75&auto=format&fit=crop",
["sander"] = "https://images.unsplash.com/photo-1581579438747-1dc8d17bbce4?w=500&q=75&auto=format&fit=crop",
["hedge-trimmer"] = "https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=500&q=75&auto=format&fit=crop",
["laser-level"] = "https://images.unsplash.com/photo-1609220136736-443140cfeaa8?w=500&q=75&auto=format&fit=crop",
["laser-measure"] = "https://images.unsplash.com/photo-1518770660439-4636190af475?w=500&q=75&auto=format&fit=crop",
};
}
@foreach (var tool in Model.Tools)
{
var img = catImages.TryGetValue(tool.CategoryId, out var u) ? u : catImages["drill"];
<div class="tool-card bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all hover:-translate-y-1 border border-gray-100"
data-cat="@tool.CategoryId">
<!-- Image -->
<div class="relative h-44 overflow-hidden">
<img src="@img" alt="تعمیر @tool.NameFa" loading="lazy"
class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
<!-- DeWalt badge -->
<div class="absolute top-3 left-3">
<span class="bg-yellow-400 text-gray-900 font-extrabold text-xs px-2 py-1 rounded-lg">DeWALT</span>
</div>
<!-- Power badge -->
<div class="absolute top-3 right-3">
<span class="bg-black/60 text-white text-xs px-2 py-1 rounded-lg font-medium">@tool.Power</span>
</div>
<!-- Icon overlay -->
<div class="absolute bottom-3 right-4 text-3xl">@tool.Icon</div>
</div>
<div class="p-5">
<!-- Title & models -->
<h3 class="font-bold text-gray-900 text-lg mb-1 leading-snug">@tool.NameFa</h3>
<p class="text-xs text-gray-400 mb-3 font-mono">@string.Join(" · ", tool.Models)</p>
<p class="text-gray-600 text-sm leading-7 mb-4">@tool.Description</p>
<!-- Repair services -->
<div class="border-t pt-4">
<p class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">خدمات تعمیر</p>
<div class="flex flex-wrap gap-1.5">
@foreach (var r in tool.RepairItems)
{
<span class="text-xs bg-yellow-50 text-yellow-800 border border-yellow-200 px-2 py-0.5 rounded-full">@r</span>
}
</div>
</div>
<!-- CTA -->
<a href="tel:02634567890"
class="mt-4 block w-full bg-yellow-400 text-gray-900 font-bold text-center py-2.5 rounded-xl hover:bg-yellow-300 transition-colors text-sm">
📞 درخواست تعمیر
</a>
</div>
</div>
}
</div>
<p id="no-results" class="hidden text-center text-gray-400 py-10">ابزاری در این دسته‌بندی یافت نشد.</p>
</div>
</section>
<!-- ═══════════ REPAIR PROCESS ═══════════ -->
<section class="py-16 px-4 bg-white">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-2">فرآیند تعمیر ابزار دیوالت</h2>
<p class="text-gray-500">ساده، شفاف و با ضمانت</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
@{
var steps = new[] {
("۱", "تحویل دستگاه", "ابزار را به‌صورت حضوری تحویل دهید یا از طریق پست ارسال کنید.", "📦"),
("۲", "بررسی رایگان", "در کمتر از ۲ ساعت عیب‌یابی کامل و برآورد قیمت ارائه می‌شود.", "🔍"),
("۳", "تعمیر با قطعه اصل", "تعمیر توسط تکنیسین مجاز DeWalt با قطعات اورجینال.", "🔧"),
("۴", "تحویل با ضمانت", "تحویل دستگاه با ضمانت‌نامه کتبی ۳ ماهه.", "🛡️"),
};
}
@foreach (var (num, title, desc, icon) in steps)
{
<div class="text-center relative">
<div class="w-16 h-16 rounded-2xl bg-yellow-400 flex items-center justify-center text-2xl mx-auto mb-4 shadow-md">
@icon
</div>
<div class="absolute top-0 right-1/2 translate-x-1/2 -translate-y-1 bg-gray-900 text-white text-xs font-bold w-5 h-5 rounded-full flex items-center justify-center">@num</div>
<h3 class="font-bold text-gray-900 mb-2">@title</h3>
<p class="text-sm text-gray-500 leading-6">@desc</p>
</div>
}
</div>
</div>
</section>
<!-- ═══════════ GENUINE PARTS SECTION ═══════════ -->
<section class="py-16 px-4" style="background:linear-gradient(135deg,#fef9c3,#fef08a)">
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-2xl font-bold text-gray-900 mb-4">قطعات اصل DeWalt</h2>
<p class="text-gray-700 leading-8 mb-6">
آساد ابزار تنها از قطعات یدکی ۱۰۰٪ اورجینال DeWalt استفاده می‌کند.
قطعات مستقیم از نمایندگی رسمی ایران تأمین می‌شود و دارای شناسه تأیید اصالت هستند.
هیچ قطعه چینی یا غیراصل در تعمیرات ما استفاده نمی‌شود.
</p>
<div class="grid grid-cols-2 gap-3">
@foreach (var part in new[] { "کاربن (ذغال) اصل", "بیرینگ اورجینال", "کلید و رئوستا", "آرمیچر و استاتور", "گیربکس کامل", "چاک ۱۳mm", "پیستون بتن‌کن", "تیغه شمشادزن" })
{
<div class="flex items-center gap-2 bg-white/60 rounded-xl px-3 py-2">
<span class="text-yellow-600 font-bold">⚡</span>
<span class="text-sm text-gray-800">@part</span>
</div>
}
</div>
</div>
<div class="relative rounded-3xl overflow-hidden shadow-xl" style="height:320px">
<img src="https://images.unsplash.com/photo-1518770660439-4636190af475?w=700&q=85&auto=format&fit=crop"
alt="قطعات یدکی اصل دیوالت" loading="lazy"
class="w-full h-full object-cover" />
</div>
</div>
</section>
<!-- ═══════════ FAQ ═══════════ -->
<section class="py-16 px-4 bg-white">
<div class="max-w-3xl mx-auto">
<h2 class="text-2xl font-bold text-gray-900 mb-8 text-center">سؤالات متداول درباره تعمیر دیوالت</h2>
<div class="space-y-4">
@{
var faqs = new[] {
("چطور بفهمم ابزار دیوالتم نیاز به تعمیر دارد؟",
"علائم رایج: جرقه داخل ابزار، کاهش قدرت، صدای غیرعادی، داغ شدن بیش از حد، لرزش شدید. در صورت مشاهده هر کدام، دستگاه را خاموش کنید و با ما تماس بگیرید."),
("هزینه تعمیر ابزار دیوالت چقدر است؟",
"هزینه بستگی به نوع عیب و مدل دارد. بررسی اولیه و برآورد قیمت کاملاً رایگان است. قبل از شروع تعمیر، قیمت دقیق به شما اعلام می‌شود."),
("آیا ضمانت تعمیر دارید؟",
"بله، تمام تعمیرات آساد ابزار دارای ضمانت‌نامه کتبی ۳ ماهه هستند. در صورت بروز مشکل مجدد در همان قطعه، رایگان تعمیر می‌شود."),
("چه مدت طول می‌کشد ابزار تعمیر شود؟",
"اکثر تعمیرات معمولی در ۲۴ تا ۴۸ ساعت انجام می‌شود. تعمیرات سنگین‌تر که نیاز به سفارش قطعه دارند ممکن است ۳ تا ۵ روز طول بکشد."),
("آیا تعمیرات از شهرهای دیگر قبول می‌کنید؟",
"بله، ابزار را می‌توانید از طریق پست یا تیپاکس ارسال کنید. ما پس از تعمیر، ابزار را به آدرس شما ارسال می‌کنیم."),
};
}
@foreach (var (q, a) in faqs)
{
<details class="group border border-gray-200 rounded-2xl overflow-hidden">
<summary class="flex items-center justify-between p-5 cursor-pointer font-semibold text-gray-800 hover:bg-gray-50">
@q
<span class="text-yellow-600 group-open:rotate-45 transition-transform shrink-0 mr-3 text-xl">+</span>
</summary>
<div class="px-5 pb-5 text-gray-600 text-sm leading-7 border-t border-gray-100 pt-4">
@a
</div>
</details>
}
</div>
</div>
</section>
<!-- ═══════════ CONTACT CTA ═══════════ -->
<section class="py-14 px-4 bg-gray-900 text-white">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-3xl font-extrabold mb-3">ابزار دیوالت خود را تعمیر کنید</h2>
<p class="text-gray-400 mb-8">تنها نمایندگی مجاز تعمیر DeWalt در کرج</p>
<div class="flex flex-wrap justify-center gap-4">
<a href="tel:02634567890" class="bg-yellow-400 text-gray-900 font-bold px-8 py-3.5 rounded-xl hover:bg-yellow-300 transition-colors text-lg">📞 ۰۲۶-۳۴۵۶۷۸۹۰</a>
<a href="https://wa.me/989123456789" class="bg-green-600 text-white font-bold px-8 py-3.5 rounded-xl hover:bg-green-700 transition-colors text-lg">💬 واتساپ</a>
<a href="/Contact" class="border-2 border-white/30 text-white font-bold px-8 py-3.5 rounded-xl hover:border-white transition-colors text-lg">📍 آدرس ما</a>
</div>
</div>
</section>
@section Scripts {
<script>
function filterTools(cat) {
const cards = document.querySelectorAll('.tool-card');
const tabs = document.querySelectorAll('.tab-btn');
let visible = 0;
cards.forEach(c => {
const show = cat === 'all' || c.dataset.cat === cat;
c.style.display = show ? '' : 'none';
if (show) visible++;
});
document.getElementById('no-results').classList.toggle('hidden', visible > 0);
tabs.forEach(t => {
const active = t.id === 'tab-' + cat;
t.classList.toggle('bg-yellow-400', active);
t.classList.toggle('text-gray-900', active);
t.classList.toggle('font-bold', active);
t.classList.toggle('text-gray-600', !active);
});
}
// Activate "all" tab on load
filterTools('all');
</script>
}
+16
View File
@@ -0,0 +1,16 @@
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.DeWalt;
public class DeWaltIndexModel : PageModel
{
public DeWaltTool[] Tools { get; } = SiteData.DeWaltTools;
public BrandInfo Brand { get; } = SiteData.Brands.First(b => b.Id == "dewalt");
public void OnGet()
{
ViewData["Title"] = "تعمیر ابزار دیوالت در کرج | نمایندگی رسمی DeWalt";
ViewData["Description"] = "نمایندگی رسمی تعمیر ابزار دیوالت (DeWalt) در کرج. تعمیر دریل، فرز، بتن‌کن، جیگ‌ساو و تمام ابزار DeWalt با قطعات اصل و ضمانت ۳ ماهه.";
}
}
+26
View File
@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
+20
View File
@@ -0,0 +1,20 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
+318
View File
@@ -0,0 +1,318 @@
@page
@model AsadiTools.Pages.IndexModel
@{
ViewData["Title"] = "آساد ابزار - نمایندگی رسمی دیوالت در کرج";
ViewData["Description"] = "نمایندگی رسمی دیوالت، ماکیتا، رونیکس، توسن و بلک اند دکر در کرج. تعمیر دریل، فرز، مینی فرز، شمشاد زن و بتن کن با ضمانت.";
Layout = "_Layout";
}
@section Head {
<script type="application/ld+json">
{"@@context":"https://schema.org","@@type":"LocalBusiness","name":"آساد ابزار کرج","image":"https://images.unsplash.com/photo-1504148455328-c376907d081c?w=800","telephone":"+98@SiteData.Company.TelPhone.Substring(1)","address":{"@@type":"PostalAddress","streetAddress":"@SiteData.Company.Address","addressLocality":"کرج","addressRegion":"البرز","postalCode":"31489","addressCountry":"IR"},"openingHours":"Mo-Sa 08:00-18:00","priceRange":"$$","sameAs":["https://instagram.com/@SiteData.Company.Instagram"]}
</script>
}
<!-- ═══════════════════════════════ HERO ═══════════════════════════════════ -->
<section class="relative text-white overflow-hidden" style="min-height:580px">
<!-- Background image with overlay -->
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat"
style="background-image:url('https://images.unsplash.com/photo-1504148455328-c376907d081c?w=1400&q=80&auto=format&fit=crop')"></div>
<div class="absolute inset-0 bg-gradient-to-l from-blue-900/95 via-blue-900/85 to-blue-800/70"></div>
<div class="relative max-w-6xl mx-auto px-4 py-20 grid md:grid-cols-2 gap-12 items-center">
<!-- Text -->
<div>
<div class="inline-flex items-center gap-2 bg-yellow-400 text-gray-900 text-sm font-bold px-4 py-1.5 rounded-full mb-5 shadow-lg">
🛡️ نمایندگی رسمی دیوالت در کرج
</div>
<h1 class="text-4xl md:text-5xl font-extrabold leading-tight mb-5">
تعمیر تخصصی<br>
<span class="text-yellow-300">ابزار صنعتی</span><br>
در کرج
</h1>
<p class="text-blue-100 text-lg leading-8 mb-8">
با بیش از ۱۵ سال تجربه، تعمیر دریل، فرز، مینی فرز، بتن‌کن و شمشاد‌زن
برندهای دیوالت، ماکیتا، رونیکس، توسن و بلک اند دکر.
</p>
<div class="flex flex-wrap gap-3 mb-10">
<a href="/Contact" class="bg-yellow-400 text-gray-900 font-bold px-6 py-3 rounded-xl hover:bg-yellow-300 transition-colors shadow-lg">📞 تماس برای تعمیر</a>
<a href="/Shop" class="border-2 border-white/80 text-white font-bold px-6 py-3 rounded-xl hover:bg-white hover:text-blue-900 transition-colors">🛒 فروشگاه قطعات</a>
<a href="/DeWalt" class="bg-white/10 border border-white/30 text-white font-bold px-6 py-3 rounded-xl hover:bg-white/20 transition-colors">🔧 ابزار دیوالت</a>
</div>
<!-- Quick trust badges -->
<div class="flex flex-wrap gap-3">
@foreach (var badge in new[] { "✅ ضمانت ۳ ماهه", "✅ قطعات اصل", "✅ تشخیص رایگان", "✅ تحویل سریع" })
{
<span class="text-sm text-blue-100 bg-white/10 px-3 py-1 rounded-full">@badge</span>
}
</div>
</div>
<!-- Stats cards -->
<div class="grid grid-cols-2 gap-4">
@foreach (var s in new[] {
("+۱۵", "سال تجربه", "🏆"),
("+۵۰۰۰", "دستگاه تعمیر شده", "🔧"),
("۵", "برند پشتیبانی", "⭐"),
("۱۰۰٪", "ضمانت تعمیر", "🛡️")
})
{
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 text-center border border-white/20 hover:bg-white/15 transition-colors">
<div class="text-3xl mb-2">@s.Item3</div>
<div class="text-3xl font-extrabold text-yellow-300 mb-1">@s.Item1</div>
<div class="text-sm text-blue-100">@s.Item2</div>
</div>
}
</div>
</div>
</section>
<!-- ══════════════════════ DEWALT FEATURED ════════════════════════════════ -->
<section class="py-16 px-4" style="background:linear-gradient(135deg,#1a1a1a 0%,#2d2d00 50%,#1a1a00 100%)">
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 items-center">
<!-- Image -->
<div class="relative rounded-3xl overflow-hidden shadow-2xl" style="height:380px">
<img src="https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=700&q=85&auto=format&fit=crop"
alt="ابزار دیوالت" class="w-full h-full object-cover" loading="lazy" />
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div class="absolute bottom-4 right-4">
<span class="bg-yellow-400 text-gray-900 font-extrabold px-4 py-2 rounded-xl text-sm shadow-lg">DEWALT</span>
</div>
</div>
<!-- Content -->
<div class="text-white">
<div class="inline-flex items-center gap-2 bg-yellow-400 text-gray-900 text-xs font-bold px-3 py-1 rounded-full mb-4">
🛡️ مجاز از DeWalt ایران
</div>
<h2 class="text-3xl font-extrabold mb-4 leading-tight">
نمایندگی رسمی<br>
<span class="text-yellow-400">DeWalt</span> در کرج
</h2>
<p class="text-gray-300 leading-8 mb-6">
آساد ابزار افتخار دارد تنها نمایندگی مجاز تعمیر ابزار دیوالت در کرج باشد.
تمامی تعمیرات توسط تکنیسین‌های آموزش‌دیده و با قطعات کاملاً اورجینال انجام می‌شود.
خدمات پشتیبانی شامل بیش از ۱۵ مدل از محبوب‌ترین ابزار دیوالت است.
</p>
<div class="grid grid-cols-2 gap-3 mb-6">
@foreach (var f in new[] { ("۱۵+", "مدل ابزار"), ("قطعه اصل", "دیوالت"), ("آموزش رسمی", "تکنیسین"), ("ضمانت کتبی", "تعمیر") })
{
<div class="bg-white/10 rounded-xl p-3 text-center border border-yellow-400/30">
<div class="font-bold text-yellow-400 text-lg">@f.Item1</div>
<div class="text-xs text-gray-300">@f.Item2</div>
</div>
}
</div>
<div class="flex gap-3">
<a href="/DeWalt" class="bg-yellow-400 text-gray-900 font-bold px-6 py-3 rounded-xl hover:bg-yellow-300 transition-colors">مشاهده همه ابزار دیوالت</a>
<a href="/Services/Brand?id=dewalt" class="border border-yellow-400/50 text-yellow-300 font-bold px-6 py-3 rounded-xl hover:border-yellow-400 transition-colors">خدمات تعمیر </a>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════ BRANDS ════════════════════════════════════════ -->
<section class="py-16 px-4 bg-white">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-2">برندهایی که تعمیر می‌کنیم</h2>
<p class="text-gray-500">تخصص ما در نمایندگی و تعمیر برترین برندهای ابزار صنعتی جهان</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
@foreach (var b in SiteData.Brands)
{
<a href="/Services/Brand?id=@b.Id"
class="group relative border-2 rounded-2xl p-5 text-center hover:shadow-xl transition-all hover:-translate-y-1"
style="border-color:@(b.IsOfficial ? "#f59e0b" : "#e5e7eb")">
@if (b.IsOfficial)
{
<div class="absolute -top-3 right-1/2 translate-x-1/2 bg-yellow-400 text-gray-900 text-xs font-bold px-2 py-0.5 rounded-full whitespace-nowrap">🛡️ رسمی</div>
}
<div class="w-16 h-16 rounded-2xl flex items-center justify-center text-2xl font-extrabold mx-auto mb-3 shadow-md"
style="background-color:@b.Color;color:@b.TextColor">
@b.Name.Substring(0, 2)
</div>
<div class="font-bold text-gray-800">@b.NameFa</div>
<div class="text-xs text-gray-400 mt-0.5">@b.Name</div>
<div class="text-xs text-blue-600 mt-2 group-hover:underline">خدمات تعمیر </div>
</a>
}
</div>
</div>
</section>
<!-- ══════════════════════ TOOLS WE REPAIR ═══════════════════════════════ -->
<section class="py-16 px-4 bg-gray-50">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-2">ابزارهایی که تعمیر می‌کنیم</h2>
<p class="text-gray-500">تخصص کامل در تعمیر انواع ابزار برقی صنعتی</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@{
var toolImages = new Dictionary<string, string>
{
["drill"] = "https://images.unsplash.com/photo-1572981779307-38b8cabb2407?w=500&q=75&auto=format&fit=crop",
["grinder"] = "https://images.unsplash.com/photo-1487452066049-a710f7296400?w=500&q=75&auto=format&fit=crop",
["mini-grinder"] = "https://images.unsplash.com/photo-1504148455328-c376907d081c?w=500&q=75&auto=format&fit=crop",
["hedge-trimmer"] = "https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=500&q=75&auto=format&fit=crop",
["rotary-hammer"] = "https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=500&q=75&auto=format&fit=crop",
["laser-level"] = "https://images.unsplash.com/photo-1609220136736-443140cfeaa8?w=500&q=75&auto=format&fit=crop",
["gerd-bar"] = "https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=500&q=75&auto=format&fit=crop",
["miter-saw"] = "https://images.unsplash.com/photo-1558618047-3c8c1d8b9df1?w=500&q=75&auto=format&fit=crop",
["woodworking"] = "https://images.unsplash.com/photo-1588854337115-1c67d9247e4d?w=500&q=75&auto=format&fit=crop",
["laser-measure"] = "https://images.unsplash.com/photo-1518770660439-4636190af475?w=500&q=75&auto=format&fit=crop",
};
}
@foreach (var t in SiteData.ToolTypes)
{
var img = toolImages.TryGetValue(t.Id, out var u) ? u : "";
<a href="/Services" class="bg-white rounded-2xl overflow-hidden border border-gray-100 hover:shadow-lg transition-all hover:-translate-y-0.5 block group">
@if (!string.IsNullOrEmpty(img))
{
<div class="h-44 overflow-hidden relative">
<img src="@img" alt="تعمیر @t.NameFa" loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
<div class="absolute bottom-3 right-4 text-3xl">@t.Icon</div>
</div>
}
<div class="p-6">
<h3 class="font-bold text-xl text-gray-900 mb-2">تعمیر @t.NameFa</h3>
<p class="text-gray-500 text-sm leading-7 mb-4">@t.Description</p>
<ul class="space-y-1.5">
@foreach (var issue in t.CommonIssues.Take(3))
{
<li class="flex items-start gap-2 text-sm text-gray-600">
<span class="text-green-500 shrink-0 mt-0.5">✓</span> @issue
</li>
}
</ul>
<div class="mt-4 text-blue-600 text-sm font-medium group-hover:underline">مشاهده جزئیات </div>
</div>
</a>
}
</div>
</div>
</section>
<!-- ══════════════════════ WORKSHOP GALLERY ═══════════════════════════════ -->
<section class="py-16 px-4 bg-white">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-2">کارگاه تعمیر آساد ابزار</h2>
<p class="text-gray-500">نگاهی به محیط کاری و تجهیزات تخصصی ما</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
@{
var gallery = new[]
{
("https://images.unsplash.com/photo-1581579438747-1dc8d17bbce4?w=600&q=80&auto=format&fit=crop", "کارگاه تعمیر ابزار"),
("https://images.unsplash.com/photo-1530124566582-a618bc2615dc?w=600&q=80&auto=format&fit=crop", "جعبه ابزار تخصصی"),
("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600&q=80&auto=format&fit=crop", "ابزار دیوالت"),
("https://images.unsplash.com/photo-1607400201515-c2c41c07d307?w=600&q=80&auto=format&fit=crop", "تکنیسین متخصص"),
("https://images.unsplash.com/photo-1518770660439-4636190af475?w=600&q=80&auto=format&fit=crop", "قطعات یدکی"),
("https://images.unsplash.com/photo-1504148455328-c376907d081c?w=600&q=80&auto=format&fit=crop", "ابزار صنعتی"),
};
}
@foreach (var (src, alt) in gallery)
{
<div class="relative rounded-2xl overflow-hidden group" style="height:200px">
<img src="@src" alt="@alt" loading="lazy"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors"></div>
</div>
}
</div>
</div>
</section>
<!-- ══════════════════════ WHY US ════════════════════════════════════════ -->
<section class="py-16 px-4 bg-gray-50">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-2">چرا آساد ابزار؟</h2>
<p class="text-gray-500">دلایل اعتماد هزاران مشتری به ما</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
@foreach (var item in new[] {
("🛡️", "ضمانت تعمیر", "ضمانت کتبی ۳ ماهه روی کلیه تعمیرات انجام‌شده", "bg-blue-50 border-blue-100"),
("🔩", "قطعات ۱۰۰٪ اصل", "فقط قطعات یدکی اورجینال از نمایندگی رسمی", "bg-yellow-50 border-yellow-100"),
("⚡", "تعمیر سریع", "اکثر تعمیرات در کمتر از ۴۸ ساعت تحویل داده می‌شود", "bg-green-50 border-green-100"),
("⭐", "تکنیسین مجرب", "بیش از ۱۵ سال سابقه تعمیر تخصصی ابزار صنعتی", "bg-purple-50 border-purple-100"),
("💰", "قیمت شفاف", "برآورد رایگان قبل از شروع تعمیر بدون هزینه پنهان", "bg-red-50 border-red-100"),
("📦", "ارسال سراسری", "تعمیر ابزار از سراسر کشور از طریق پست و تیپاکس", "bg-orange-50 border-orange-100"),
("📞", "مشاوره رایگان", "مشاوره تلفنی رایگان برای تشخیص عیب دستگاه", "bg-teal-50 border-teal-100"),
("🔄", "خدمات پیشگیرانه", "سرویس دوره‌ای و تنظیم ابزار برای جلوگیری از خرابی", "bg-indigo-50 border-indigo-100"),
})
{
<div class="text-center p-6 rounded-2xl border @item.Item4">
<div class="text-3xl mb-3">@item.Item1</div>
<h3 class="font-bold text-gray-900 mb-2">@item.Item2</h3>
<p class="text-sm text-gray-500 leading-6">@item.Item3</p>
</div>
}
</div>
</div>
</section>
<!-- ══════════════════════ TECHNICIAN SECTION ════════════════════════════ -->
<section class="py-16 px-4 bg-blue-900 text-white">
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-3xl font-extrabold mb-4">تکنیسین‌های ما، متخصص‌ترین در البرز</h2>
<p class="text-blue-100 leading-8 mb-6">
تیم تکنیسین‌های آساد ابزار با گذراندن دوره‌های آموزشی رسمی دیوالت و ماکیتا،
تجربه تعمیر بیش از ۵۰۰۰ دستگاه را دارند. از ساده‌ترین مشکل مانند تعویض کاربن،
تا پیچیده‌ترین عیوب الکترونیکی و مکانیکی.
</p>
<div class="space-y-3">
@foreach (var f in new[] {
"آموزش رسمی از DeWalt و Makita",
"تجربه بیش از ۱۵ سال تعمیر تخصصی",
"استفاده از دستگاه‌های اندازه‌گیری حرفه‌ای",
"به‌روز بودن با آخرین مدل‌های ابزار",
"ارائه رسید و فاکتور رسمی برای تمام تعمیرات",
})
{
<div class="flex items-center gap-3">
<span class="w-5 h-5 rounded-full bg-yellow-400 flex items-center justify-center text-gray-900 text-xs font-bold shrink-0">✓</span>
<span class="text-blue-100 text-sm">@f</span>
</div>
}
</div>
</div>
<div class="relative rounded-3xl overflow-hidden shadow-2xl" style="height:380px">
<img src="https://images.unsplash.com/photo-1581579438747-1dc8d17bbce4?w=700&q=85&auto=format&fit=crop"
alt="تکنیسین تعمیر ابزار آساد ابزار کرج" loading="lazy"
class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-blue-900/80 to-transparent"></div>
<div class="absolute bottom-5 right-5 left-5">
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20">
<div class="font-bold text-sm mb-1">آساد ابزار کرج</div>
<div class="text-blue-200 text-xs">نمایندگی رسمی دیوالت • ۱۵ سال تجربه</div>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════════════ CTA ═══════════════════════════════════════════ -->
<section class="py-16 px-4 bg-yellow-400">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-3xl font-extrabold text-gray-900 mb-4">ابزار شما خراب شده؟</h2>
<p class="text-gray-700 text-lg mb-8">همین الان با ما تماس بگیرید. مشاوره رایگان و تعمیر با ضمانت کتبی.</p>
<div class="flex flex-wrap justify-center gap-4">
<a href="tel:@SiteData.Company.TelPhone" class="bg-gray-900 text-white font-bold px-8 py-3.5 rounded-xl hover:bg-gray-800 transition-colors text-lg shadow-lg">
📞 @SiteData.Company.Phone
</a>
<a href="https://wa.me/@SiteData.Company.Whatsapp" target="_blank"
class="bg-green-600 text-white font-bold px-8 py-3.5 rounded-xl hover:bg-green-700 transition-colors text-lg shadow-lg">
💬 واتساپ
</a>
<a href="/DeWalt" class="bg-white text-gray-900 font-bold px-8 py-3.5 rounded-xl hover:bg-gray-50 transition-colors text-lg shadow-lg">
🔧 ابزار دیوالت
</a>
</div>
</div>
</section>
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
+8
View File
@@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages;
public class PrivacyModel : PageModel
{
public void OnGet()
{
}
}
+218
View File
@@ -0,0 +1,218 @@
@page
@model AsadiTools.Pages.Services.BrandModel
@{ ViewData["Title"] = $"تعمیر ابزار {Model.Brand?.NameFa} در کرج"; Layout = "_Layout"; }
@if (Model.Brand is null)
{
<div class="max-w-xl mx-auto text-center py-20"><p class="text-gray-500">برند یافت نشد.</p></div>
}
else
{
var b = Model.Brand;
var isDewalt = b.Id == "dewalt";
<!-- ═══════════ HERO ═══════════ -->
<div class="relative text-white py-14 px-4 overflow-hidden" style="background:linear-gradient(135deg,@b.Color+dd,@b.Color+99)">
@if (isDewalt)
{
<div class="absolute inset-0 bg-cover bg-center opacity-20"
style="background-image:url('https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200&q=80&auto=format&fit=crop')"></div>
}
<div class="relative max-w-6xl mx-auto flex flex-col sm:flex-row items-center gap-6">
<div class="w-20 h-20 rounded-2xl flex items-center justify-center text-2xl font-extrabold border-2 border-white/30 bg-white/20 shrink-0 shadow-lg"
style="color:@b.TextColor">
@b.Name.Substring(0, 2)
</div>
<div>
<div class="flex flex-wrap items-center gap-3 mb-2">
<h1 class="text-3xl font-extrabold" style="color:@(b.TextColor == "#fff" ? "white" : "#1f2937")">
تعمیر ابزار @b.NameFa در کرج
</h1>
@if (b.IsOfficial)
{
<span class="bg-white text-gray-900 text-sm font-bold px-3 py-1 rounded-full shadow">🛡️ نمایندگی رسمی</span>
}
</div>
<p class="opacity-90 max-w-xl" style="color:@(b.TextColor == "#fff" ? "#e0e0e0" : "#374151")">@b.Description</p>
@if (isDewalt)
{
<a href="/DeWalt" class="inline-block mt-3 bg-yellow-400 text-gray-900 font-bold text-sm px-4 py-2 rounded-lg hover:bg-yellow-300 transition-colors">
مشاهده همه ابزار دیوالت ›
</a>
}
</div>
</div>
</div>
<div class="max-w-6xl mx-auto px-4 py-12 grid lg:grid-cols-3 gap-10">
<!-- ═══════════ MAIN CONTENT ═══════════ -->
<div class="lg:col-span-2 space-y-8">
@if (b.IsOfficial)
{
<div class="bg-yellow-50 border border-yellow-200 rounded-2xl p-6">
<div class="flex items-start gap-3">
<span class="text-yellow-600 text-2xl shrink-0">🛡️</span>
<div>
<h2 class="font-bold text-gray-900 mb-2 text-lg">نمایندگی رسمی @b.NameFa در کرج</h2>
<p class="text-gray-600 text-sm leading-7">
آساد ابزار افتخار دارد نمایندگی رسمی برند @b.NameFa را در شهر کرج داشته باشد.
تمام تعمیرات توسط تکنیسین‌های آموزش‌دیده و با قطعات کاملاً اورجینال انجام می‌شود.
هر تعمیر دارای ضمانت‌نامه کتبی ۳ ماهه است.
</p>
</div>
</div>
</div>
}
<!-- DeWalt: full tools grid -->
@if (isDewalt)
{
<section>
<h2 class="text-xl font-bold text-gray-900 mb-5 pb-2 border-b flex items-center gap-2">
<span class="bg-yellow-400 text-gray-900 text-xs font-bold px-2 py-0.5 rounded">۱۵+ مدل</span>
ابزار دیوالت که تعمیر می‌کنیم
</h2>
<div class="grid sm:grid-cols-2 gap-4">
@{
var catImgs = new Dictionary<string, string>
{
["drill"] = "https://images.unsplash.com/photo-1572981779307-38b8cabb2407?w=400&q=70&auto=format&fit=crop",
["driver"] = "https://images.unsplash.com/photo-1530124566582-a618bc2615dc?w=400&q=70&auto=format&fit=crop",
["grinder"] = "https://images.unsplash.com/photo-1487452066049-a710f7296400?w=400&q=70&auto=format&fit=crop",
["rotary-hammer"] = "https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=400&q=70&auto=format&fit=crop",
["saw"] = "https://images.unsplash.com/photo-1504148455328-c376907d081c?w=400&q=70&auto=format&fit=crop",
["chop-saw"] = "https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=400&q=70&auto=format&fit=crop",
["miter-saw"] = "https://images.unsplash.com/photo-1558618047-3c8c1d8b9df1?w=400&q=70&auto=format&fit=crop",
["woodworking"] = "https://images.unsplash.com/photo-1588854337115-1c67d9247e4d?w=400&q=70&auto=format&fit=crop",
["multi"] = "https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&q=70&auto=format&fit=crop",
["sander"] = "https://images.unsplash.com/photo-1581579438747-1dc8d17bbce4?w=400&q=70&auto=format&fit=crop",
["hedge-trimmer"] = "https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=400&q=70&auto=format&fit=crop",
["laser-level"] = "https://images.unsplash.com/photo-1609220136736-443140cfeaa8?w=400&q=70&auto=format&fit=crop",
["laser-measure"] = "https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&q=70&auto=format&fit=crop",
};
}
@foreach (var tool in SiteData.DeWaltTools)
{
var img = catImgs.TryGetValue(tool.CategoryId, out var tu) ? tu : catImgs["drill"];
<div class="bg-white rounded-2xl border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
<div class="relative h-36 overflow-hidden">
<img src="@img" alt="تعمیر @tool.NameFa" loading="lazy"
class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div class="absolute top-2 left-2">
<span class="bg-yellow-400 text-gray-900 text-xs font-bold px-1.5 py-0.5 rounded">@tool.Power</span>
</div>
<div class="absolute bottom-2 right-3 text-2xl">@tool.Icon</div>
</div>
<div class="p-4">
<h3 class="font-bold text-gray-900 text-sm mb-0.5">@tool.NameFa</h3>
<p class="text-xs text-gray-400 font-mono mb-2">@string.Join(" · ", tool.Models.Take(3))</p>
<div class="flex flex-wrap gap-1">
@foreach (var r in tool.RepairItems.Take(3))
{
<span class="text-xs bg-yellow-50 text-yellow-800 border border-yellow-100 px-1.5 py-0.5 rounded">@r</span>
}
@if (tool.RepairItems.Length > 3)
{
<span class="text-xs text-gray-400">+@(tool.RepairItems.Length - 3) مورد دیگر</span>
}
</div>
</div>
</div>
}
</div>
<div class="mt-6 text-center">
<a href="/DeWalt" class="inline-block bg-yellow-400 text-gray-900 font-bold px-6 py-3 rounded-xl hover:bg-yellow-300 transition-colors">
مشاهده صفحه کامل ابزار دیوالت ›
</a>
</div>
</section>
}
else
{
<!-- Non-DeWalt brands: existing tool service cards -->
<section>
<h2 class="text-xl font-bold text-gray-900 mb-5 pb-2 border-b">خدمات تعمیر @b.NameFa</h2>
<div class="space-y-4">
@foreach (var tool in SiteData.ToolTypes.Where(t => b.Services.Contains(t.NameFa)))
{
<div class="bg-white rounded-2xl border border-gray-100 p-6 hover:shadow-sm transition-shadow">
<div class="flex items-center gap-3 mb-3">
<span class="text-2xl">@tool.Icon</span>
<h3 class="font-bold text-gray-900">تعمیر @tool.NameFa @b.NameFa</h3>
</div>
<p class="text-gray-500 text-sm leading-7 mb-4">@tool.Description</p>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@foreach (var issue in tool.CommonIssues)
{
<li class="flex items-start gap-2 text-sm text-gray-600">
<span class="text-green-500 shrink-0">✓</span> @issue
</li>
}
</ul>
</div>
}
</div>
</section>
}
<!-- Repair process -->
<section>
<h2 class="text-xl font-bold text-gray-900 mb-5 pb-2 border-b">فرآیند تعمیر</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@foreach (var step in new[] { ("۱","تحویل ابزار","حضوری یا پست"), ("۲","بررسی رایگان","تشخیص سریع"), ("۳","تعمیر تخصصی","قطعه اصل"), ("۴","تحویل با ضمانت","۳ ماه گارانتی") })
{
<div class="text-center p-4 bg-gray-50 rounded-2xl">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold mx-auto mb-3 shadow"
style="background-color:@b.Color;color:@b.TextColor">@step.Item1</div>
<div class="font-bold text-gray-800 text-sm">@step.Item2</div>
<div class="text-xs text-gray-400 mt-1">@step.Item3</div>
</div>
}
</div>
</section>
</div>
<!-- ═══════════ SIDEBAR ═══════════ -->
<div class="space-y-5">
<div class="rounded-2xl p-6 text-white" style="background-color:@b.Color">
<h3 class="font-bold text-lg mb-1" style="color:@(b.TextColor == "#fff" ? "white" : "#1f2937")">درخواست تعمیر</h3>
<p class="text-sm mb-5 opacity-80" style="color:@(b.TextColor == "#fff" ? "#e0e0e0" : "#374151")">مشاوره رایگان – تماس بگیرید</p>
<a href="tel:@SiteData.Company.TelPhone" class="block bg-white font-bold text-center py-3 rounded-xl hover:opacity-90 mb-3 transition-opacity"
style="color:@b.Color">📞 @SiteData.Company.Phone</a>
<a href="https://wa.me/@SiteData.Company.Whatsapp" target="_blank"
class="block bg-green-500 text-white font-bold text-center py-3 rounded-xl hover:bg-green-600 transition-colors">💬 واتساپ</a>
</div>
<div class="bg-white rounded-2xl border p-6">
<h3 class="font-bold text-gray-900 mb-4">⭐ مزایای خدمات ما</h3>
<ul class="space-y-2.5 text-sm text-gray-600">
@foreach (var item in new[] { "بررسی و تشخیص عیب رایگان", "قطعات ۱۰۰٪ اورجینال", "ضمانت‌نامه کتبی ۳ ماهه", "تکنیسین مجاز و متخصص", "قیمت شفاف قبل از شروع کار", "تحویل سریع زیر ۴۸ ساعت", "ارسال و دریافت از سراسر کشور" })
{
<li class="flex items-center gap-2">
<span class="text-green-500 shrink-0">✓</span> @item
</li>
}
</ul>
</div>
@if (isDewalt)
{
<div class="bg-yellow-50 border border-yellow-200 rounded-2xl p-5">
<h3 class="font-bold text-gray-900 mb-3">🔩 قطعات موجود DeWalt</h3>
<div class="flex flex-wrap gap-1.5">
@foreach (var p in new[] { "کاربن", "بیرینگ", "آرمیچر", "گیربکس", "چاک", "کلید", "استاتور", "پیستون" })
{
<span class="text-xs bg-yellow-100 text-yellow-800 border border-yellow-200 px-2 py-0.5 rounded-full">@p</span>
}
</div>
</div>
}
<a href="/Shop" class="block bg-gray-50 border border-gray-200 rounded-2xl p-5 hover:shadow-md transition-shadow">
<h3 class="font-bold text-gray-900 mb-1">🛒 فروشگاه قطعات</h3>
<p class="text-sm text-gray-500">قطعات یدکی @b.NameFa را مستقیم سفارش دهید.</p>
</a>
</div>
</div>
}
+16
View File
@@ -0,0 +1,16 @@
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Services;
public class BrandModel : PageModel
{
public BrandInfo? Brand { get; private set; }
public void OnGet(string id)
{
Brand = SiteData.Brands.FirstOrDefault(b => b.Id == id);
if (Brand is not null)
ViewData["Description"] = $"{Brand.Description} تعمیر {string.Join("، ", Brand.Services)} {Brand.NameFa} در کرج.";
}
}
+62
View File
@@ -0,0 +1,62 @@
@page
@model AsadiTools.Pages.Services.ServicesIndexModel
@{ ViewData["Title"] = "خدمات تعمیر ابزار برقی در کرج"; Layout = "_Layout"; }
<div class="bg-blue-800 text-white py-12 px-4">
<div class="max-w-6xl mx-auto">
<h1 class="text-3xl font-extrabold mb-2">خدمات تعمیر ابزار</h1>
<p class="text-blue-200 text-lg">تعمیر تخصصی انواع ابزار برقی صنعتی توسط تکنیسین‌های مجرب در کرج</p>
</div>
</div>
<div class="max-w-6xl mx-auto px-4 py-12">
<section class="mb-14">
<h2 class="text-xl font-bold text-gray-900 mb-6 pb-2 border-b">خدمات بر اساس برند</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@foreach (var b in SiteData.Brands)
{
<a href="/Services/Brand?id=@b.Id" class="group bg-white rounded-2xl border border-gray-100 p-6 hover:shadow-lg transition-all hover:-translate-y-0.5">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-xl font-extrabold shrink-0"
style="background-color:@b.Color;color:@b.TextColor">@b.Name[0]</div>
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-gray-900">@b.NameFa</span>
@if (b.IsOfficial) { <span class="text-xs bg-yellow-400 text-gray-900 px-1.5 py-0.5 rounded font-bold">رسمی</span> }
</div>
<div class="text-xs text-gray-400">@b.Name</div>
</div>
</div>
<p class="text-sm text-gray-600 mb-4 leading-7">@b.Description</p>
<div class="flex flex-wrap gap-1.5 mb-3">
@foreach (var svc in b.Services) { <span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">@svc</span> }
</div>
<div class="text-sm text-blue-600 font-medium">مشاهده خدمات کامل ›</div>
</a>
}
</div>
</section>
<section>
<h2 class="text-xl font-bold text-gray-900 mb-6 pb-2 border-b">خدمات بر اساس نوع ابزار</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach (var t in SiteData.ToolTypes)
{
<div class="bg-white rounded-2xl border border-gray-100 p-6">
<div class="flex items-center gap-3 mb-4">
<span class="text-3xl">@t.Icon</span>
<h3 class="font-bold text-xl text-gray-900">@t.NameFa</h3>
</div>
<p class="text-gray-500 text-sm leading-7 mb-4">@t.Description</p>
<p class="text-sm font-bold text-gray-700 mb-3">مشکلات رایج:</p>
<ul class="space-y-2">
@foreach (var issue in t.CommonIssues)
{
<li class="flex items-start gap-2 text-sm text-gray-600"><span class="text-green-500 shrink-0">✓</span> @issue</li>
}
</ul>
</div>
}
</div>
</section>
</div>
+8
View File
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Services;
public class ServicesIndexModel : PageModel
{
public void OnGet() { }
}
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] | پنل مدیریت آساد ابزار</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2") format("woff2"); font-weight:400; font-display:swap; }
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Bold.woff2") format("woff2"); font-weight:700; font-display:swap; }
*, body { font-family: "Vazirmatn", Tahoma, Arial, sans-serif !important; }
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="fixed right-0 top-0 h-full w-56 bg-gray-900 text-white flex flex-col z-40">
<div class="p-5 border-b border-gray-800">
<div class="flex items-center gap-2">
<div class="bg-blue-600 rounded-lg p-1.5 text-sm">🔧</div>
<div>
<div class="font-bold text-sm">آساد ابزار</div>
<div class="text-xs text-gray-400">پنل مدیریت</div>
</div>
</div>
</div>
<nav class="flex-1 p-4 space-y-1">
<a href="/Admin" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
📊 داشبورد
</a>
<a href="/Admin/Products" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
📦 محصولات
</a>
<a href="/Admin/Orders" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
🛍️ سفارش‌ها
</a>
<a href="/Admin/Blog" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
📝 بلاگ
</a>
<a href="/Admin/ChangePassword" class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
🔑 تغییر رمز عبور
</a>
</nav>
<div class="p-4 border-t border-gray-800">
<form method="post" action="/Admin/Logout">
@Html.AntiForgeryToken()
<button type="submit" class="text-sm text-red-400 hover:text-red-300 transition-colors">🚪 خروج</button>
</form>
</div>
</aside>
<!-- Main -->
<main class="flex-1 mr-56 overflow-auto">
@RenderBody()
</main>
</div>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
@inject AsadiTools.Services.CartService Cart
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@(ViewData["Title"] != null ? ViewData["Title"] + " | آساد ابزار کرج" : "آساد ابزار - نمایندگی رسمی دیوالت در کرج")</title>
<meta name="description" content="@(ViewData["Description"] ?? "نمایندگی رسمی دیوالت، ماکیتا، رونیکس، توسن و بلک اند دکر در کرج. تعمیر دریل، فرز، مینی فرز، شمشاد زن و بتن کن.")" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<script src="https://cdn.tailwindcss.com"></script>
<style>
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2") format("woff2"); font-weight:400; font-display:swap; }
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-Bold.woff2") format("woff2"); font-weight:700; font-display:swap; }
@@font-face { font-family:"Vazirmatn"; src:url("https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@@v33.003/fonts/webfonts/Vazirmatn-ExtraBold.woff2") format("woff2"); font-weight:800; font-display:swap; }
*, body { font-family: "Vazirmatn", Tahoma, Arial, sans-serif !important; }
::-webkit-scrollbar { width:5px } ::-webkit-scrollbar-thumb { background:#94a3b8; border-radius:3px }
</style>
@await RenderSectionAsync("Head", required: false)
</head>
<body class="bg-gray-50 text-gray-800 flex flex-col min-h-screen">
<!-- Top bar -->
<div class="bg-blue-800 text-white text-sm py-1.5 px-4">
<div class="max-w-6xl mx-auto flex justify-between items-center">
<a href="tel:@SiteData.Company.TelPhone" class="hover:text-yellow-300 transition-colors">📞 @SiteData.Company.Phone</a>
<span class="text-yellow-300 font-bold text-xs">نمایندگی رسمی دیوالت در کرج</span>
</div>
</div>
<!-- Header -->
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-2 shrink-0">
<div class="bg-blue-700 text-white rounded-lg p-2 text-lg">🔧</div>
<div>
<div class="font-bold text-lg text-gray-900 leading-tight">آساد ابزار</div>
<div class="text-xs text-gray-400">کرج • تعمیر ابزار صنعتی</div>
</div>
</a>
<nav class="hidden md:flex items-center gap-5">
<a href="/" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">خانه</a>
<a href="/Services" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">خدمات</a>
<a href="/brands" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">برندها</a>
<a href="/blog" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">بلاگ</a>
<a href="/DeWalt" class="inline-flex items-center gap-1 bg-yellow-400 text-gray-900 font-bold px-3 py-1 rounded-lg text-sm hover:bg-yellow-300 transition-colors">🛡️ دیوالت</a>
<a href="/Shop" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">فروشگاه قطعات</a>
<a href="/Contact" class="text-gray-600 hover:text-blue-700 font-medium transition-colors text-sm">تماس با ما</a>
</nav>
<div class="flex items-center gap-3">
<a href="/Cart" class="relative flex items-center gap-1.5 bg-blue-700 text-white px-3 py-2 rounded-lg text-sm font-medium hover:bg-blue-800 transition-colors">
🛒 <span class="hidden sm:inline">سبد خرید</span>
@if (Cart.Count > 0)
{
<span class="absolute -top-1.5 -left-1.5 bg-yellow-400 text-gray-900 text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">@Cart.Count</span>
}
</a>
<button onclick="document.getElementById('mobile-menu').classList.toggle('hidden')" class="md:hidden p-2 rounded-lg hover:bg-gray-100">☰</button>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t bg-white px-4 py-3 space-y-1">
<a href="/" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">خانه</a>
<a href="/Services" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">خدمات</a>
<a href="/brands" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">برندها</a>
<a href="/blog" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">بلاگ</a>
<a href="/DeWalt" class="block py-2.5 px-3 rounded-lg bg-yellow-50 text-gray-900 font-bold">🛡️ ابزار دیوالت (رسمی)</a>
<a href="/Shop" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">فروشگاه قطعات</a>
<a href="/Contact" class="block py-2.5 px-3 rounded-lg hover:bg-blue-50 hover:text-blue-700 font-medium">تماس با ما</a>
</div>
</header>
<main class="flex-1">
@RenderBody()
</main>
<!-- Footer -->
<footer class="bg-gray-900 text-gray-300 mt-auto">
<div class="max-w-6xl mx-auto px-4 py-12 grid grid-cols-1 md:grid-cols-3 gap-10">
<div>
<h3 class="text-white font-bold text-lg mb-4">آساد ابزار</h3>
<p class="text-sm leading-7 text-gray-400">با بیش از ۱۵ سال تجربه، نمایندگی رسمی دیوالت در کرج. تعمیر تخصصی ابزار صنعتی با قطعات اصل.</p>
</div>
<div>
<h3 class="text-white font-bold text-lg mb-4">تماس</h3>
<ul class="space-y-2 text-sm">
<li>📞 <a href="tel:@SiteData.Company.TelPhone" class="hover:text-white">@SiteData.Company.Phone</a></li>
<li>📱 <a href="tel:@SiteData.Company.TelMobile" class="hover:text-white">@SiteData.Company.Mobile</a></li>
<li>📍 @SiteData.Company.Address</li>
<li>🕐 @SiteData.Company.WorkingHours</li>
</ul>
</div>
<div>
<h3 class="text-white font-bold text-lg mb-4">خدمات</h3>
<ul class="space-y-2 text-sm">
<li><a href="/Services/Brand?id=dewalt" class="hover:text-white">تعمیر ابزار دیوالت <span class="text-yellow-400 text-xs">(رسمی)</span></a></li>
<li><a href="/Services/Brand?id=makita" class="hover:text-white">تعمیر ابزار ماکیتا</a></li>
<li><a href="/Services/Brand?id=ronix" class="hover:text-white">تعمیر ابزار رونیکس</a></li>
<li><a href="/Services/Brand?id=tosan" class="hover:text-white">تعمیر ابزار توسن</a></li>
<li><a href="/Shop" class="hover:text-white">فروشگاه قطعات یدکی</a></li>
</ul>
</div>
</div>
<div class="border-t border-gray-800 py-4 text-center text-xs text-gray-500">
© @DateTime.Now.Year آساد ابزار کرج – تمام حقوق محفوظ است
</div>
</footer>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}
@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>
+74
View File
@@ -0,0 +1,74 @@
@page
@model AsadiTools.Pages.Shop.DetailModel
@{ ViewData["Title"] = Model.Product?.NameFa ?? "قطعه"; Layout = "_Layout"; }
@if (Model.Product is null)
{
<div class="max-w-xl mx-auto text-center py-20"><p class="text-gray-500">محصول یافت نشد.</p><a href="/Shop" class="text-blue-600 hover:underline mt-2 block">بازگشت به فروشگاه</a></div>
}
else
{
var p = Model.Product;
var cat = SiteData.Categories.FirstOrDefault(c => c.Id == p.Category);
var brand = SiteData.Brands.FirstOrDefault(b => b.Id == p.Brand);
<div class="max-w-5xl mx-auto px-4 py-8">
<nav class="flex items-center gap-2 text-sm text-gray-500 mb-8">
<a href="/" class="hover:text-blue-600">خانه</a><span>/</span>
<a href="/Shop" class="hover:text-blue-600">فروشگاه</a><span>/</span>
<span class="text-gray-800">@p.NameFa</span>
</nav>
<div class="grid md:grid-cols-2 gap-10">
<div class="rounded-3xl overflow-hidden aspect-square @(string.IsNullOrEmpty(p.ImageUrl) ? "bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center text-8xl" : "")">
@if (!string.IsNullOrEmpty(p.ImageUrl))
{
<img src="@p.ImageUrl" alt="@p.NameFa" class="w-full h-full object-cover" />
}
else
{
@(cat?.Icon ?? "🔧")
}
</div>
<div>
<div class="flex flex-wrap gap-2 mb-4">
@if (brand != null) { <span class="text-sm font-bold px-3 py-1 rounded-full text-white" style="background-color:@brand.Color">@brand.NameFa</span> }
@if (cat != null) { <span class="text-sm px-3 py-1 rounded-full bg-gray-100 text-gray-600">@cat.Icon @cat.NameFa</span> }
</div>
<h1 class="text-2xl font-extrabold text-gray-900 mb-2">@p.NameFa</h1>
@if (p.NameEn != null) { <p class="text-gray-400 text-sm mb-4 font-mono">@p.NameEn</p> }
@if (p.Sku != null) { <p class="text-xs text-gray-400 mb-6 bg-gray-100 inline-block px-3 py-1 rounded-full">کد محصول: @p.Sku</p> }
@if (p.Description != null) { <p class="text-gray-600 leading-7 mb-6 text-sm">@p.Description</p> }
<div class="mb-6">
@if (p.HasDiscount) { <p class="text-gray-400 line-through text-lg">@SiteData.FormatPrice(p.Price)</p> }
<p class="text-3xl font-extrabold text-blue-700">@SiteData.FormatPrice(p.FinalPrice)</p>
</div>
<div class="mb-6">
@if (p.Stock > 0)
{
<span class="text-green-600 text-sm font-medium">🟢 موجود در انبار (@p.Stock عدد)</span>
}
else
{
<span class="text-red-500 text-sm font-medium">🔴 ناموجود</span>
}
</div>
<form method="post" class="flex gap-3">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@p.Id" />
<input type="hidden" name="nameFa" value="@p.NameFa" />
<input type="hidden" name="price" value="@p.FinalPrice" />
<input type="hidden" name="sku" value="@p.Sku" />
<button type="submit" @(p.Stock == 0 ? "disabled" : "")
class="flex-1 flex items-center justify-center gap-2 bg-blue-700 text-white py-3.5 rounded-xl font-bold hover:bg-blue-800 transition-colors disabled:bg-gray-200 disabled:text-gray-400">
🛒 @(p.Stock == 0 ? "ناموجود" : "افزودن به سبد خرید")
</button>
<a href="/Cart" class="flex items-center gap-2 border-2 border-blue-700 text-blue-700 py-3.5 px-4 rounded-xl font-bold hover:bg-blue-50">سبد</a>
</form>
</div>
</div>
</div>
}
+23
View File
@@ -0,0 +1,23 @@
using AsadiTools.Data;
using AsadiTools.Models;
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AsadiTools.Pages.Shop;
public class DetailModel(AppDbContext db, CartService cart) : PageModel
{
public Product? Product { get; private set; }
public async Task OnGetAsync(int id)
{
Product = await db.Products.FindAsync(id);
}
public IActionResult OnPost(int productId, string nameFa, decimal price, string? sku)
{
cart.AddItem(new CartItem { ProductId = productId, NameFa = nameFa, Price = price, Sku = sku, Qty = 1 });
return RedirectToPage("/Cart/Index");
}
}
+139
View File
@@ -0,0 +1,139 @@
@page
@model AsadiTools.Pages.Shop.ShopIndexModel
@{ ViewData["Title"] = "فروشگاه قطعات یدکی ابزار برقی"; Layout = "_Layout"; }
<div class="bg-blue-800 text-white py-10 px-4">
<div class="max-w-6xl mx-auto">
<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-8">
<!-- Filters -->
<div class="bg-white rounded-2xl border border-gray-100 p-5 mb-8">
<form method="get" class="mb-5">
<input type="text" name="search" value="@Model.Search" placeholder="جستجو در قطعات..."
class="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" />
</form>
<div class="mb-4">
<p class="text-xs text-gray-500 font-bold mb-2">دسته‌بندی</p>
<div class="flex flex-wrap gap-2">
<a href="/Shop" class="text-sm px-3 py-1.5 rounded-full border transition-colors @(Model.Category == null ? "bg-blue-700 text-white border-blue-700" : "border-gray-200 text-gray-600 hover:border-blue-400")">همه</a>
@foreach (var cat in SiteData.Categories)
{
<a href="/Shop?category=@cat.Id@(Model.Brand != null ? "&brand=" + Model.Brand : "")"
class="text-sm px-3 py-1.5 rounded-full border transition-colors @(Model.Category == cat.Id ? "bg-blue-700 text-white border-blue-700" : "border-gray-200 text-gray-600 hover:border-blue-400")">
@cat.Icon @cat.NameFa
</a>
}
</div>
</div>
<div>
<p class="text-xs text-gray-500 font-bold mb-2">برند</p>
<div class="flex flex-wrap gap-2">
<a href="/Shop@(Model.Category != null ? "?category=" + Model.Category : "")"
class="text-sm px-3 py-1.5 rounded-full border transition-colors @(Model.Brand == null ? "bg-blue-700 text-white border-blue-700" : "border-gray-200 text-gray-600 hover:border-blue-400")">همه برندها</a>
@foreach (var brand in SiteData.Brands)
{
<a href="/Shop?brand=@brand.Id@(Model.Category != null ? "&category=" + Model.Category : "")"
class="text-sm px-3 py-1.5 rounded-full border transition-colors @(Model.Brand == brand.Id ? "bg-blue-700 text-white border-blue-700" : "border-gray-200 text-gray-600 hover:border-blue-400")">
@brand.NameFa
</a>
}
</div>
</div>
</div>
<p class="text-sm text-gray-500 mb-5">@Model.TotalCount محصول یافت شد</p>
@if (!Model.Products.Any())
{
<div class="text-center py-20 text-gray-400">
<div class="text-5xl mb-4">🔍</div>
<p>محصولی یافت نشد</p>
<a href="/Shop" class="text-blue-600 text-sm mt-2 hover:underline block">مشاهده همه محصولات</a>
</div>
}
else
{
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
@foreach (var p in Model.Products)
{
var cat = SiteData.Categories.FirstOrDefault(c => c.Id == p.Category);
var brand = SiteData.Brands.FirstOrDefault(b => b.Id == p.Brand);
<div class="bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow flex flex-col">
@if (!string.IsNullOrEmpty(p.ImageUrl))
{
<div class="rounded-t-xl overflow-hidden" style="height:160px">
<img src="@p.ImageUrl" alt="@p.NameFa" loading="lazy"
class="w-full h-full object-cover"
onerror="this.parentElement.innerHTML='<div class=\'bg-gradient-to-br from-blue-50 to-blue-100 h-full flex items-center justify-center text-5xl\'>@(cat?.Icon ?? "🔧")</div>'" />
</div>
}
else
{
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-t-xl p-8 flex items-center justify-center text-5xl">
@(cat?.Icon ?? "🔧")
</div>
}
<div class="p-4 flex flex-col flex-1 gap-3">
<div class="flex flex-wrap gap-1.5">
@if (brand != null) { <span class="text-xs font-bold px-2 py-0.5 rounded-full text-white" style="background-color:@brand.Color">@brand.NameFa</span> }
@if (cat != null) { <span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-600">@cat.NameFa</span> }
@if (p.Stock == 0) { <span class="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-600">ناموجود</span> }
</div>
<div>
<a href="/Shop/Detail?id=@p.Id" class="font-bold text-gray-900 hover:text-blue-700 transition-colors leading-snug block">@p.NameFa</a>
@if (p.Sku != null) { <p class="text-xs text-gray-400 mt-0.5">کد: @p.Sku</p> }
</div>
<div class="mt-auto">
@if (p.HasDiscount) { <p class="text-sm text-gray-400 line-through">@SiteData.FormatPrice(p.Price)</p> }
<p class="text-lg font-bold text-blue-700">@SiteData.FormatPrice(p.FinalPrice)</p>
</div>
<form method="post" asp-page-handler="AddToCart">
@Html.AntiForgeryToken()
<input type="hidden" name="productId" value="@p.Id" />
<input type="hidden" name="nameFa" value="@p.NameFa" />
<input type="hidden" name="price" value="@p.FinalPrice" />
<input type="hidden" name="sku" value="@p.Sku" />
<button type="submit" @(p.Stock == 0 ? "disabled" : "")
class="w-full flex items-center justify-center gap-2 bg-blue-700 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-blue-800 transition-colors disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed">
🛒 @(p.Stock == 0 ? "ناموجود" : "افزودن به سبد")
</button>
</form>
</div>
</div>
}
</div>
}
@if (Model.TotalPages > 1)
{
<div class="flex justify-center items-center gap-2 mt-10">
@if (Model.CurrentPage > 1)
{
<a href="/Shop?page=@(Model.CurrentPage - 1)@(Model.Category != null ? "&category=" + Model.Category : "")@(Model.Brand != null ? "&brand=" + Model.Brand : "")@(Model.Search != null ? "&search=" + Model.Search : "")"
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="/Shop?page=@i@(Model.Category != null ? "&category=" + Model.Category : "")@(Model.Brand != null ? "&brand=" + Model.Brand : "")@(Model.Search != null ? "&search=" + Model.Search : "")"
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 hover:text-blue-700")">
@i
</a>
}
@if (Model.CurrentPage < Model.TotalPages)
{
<a href="/Shop?page=@(Model.CurrentPage + 1)@(Model.Category != null ? "&category=" + Model.Category : "")@(Model.Brand != null ? "&brand=" + Model.Brand : "")@(Model.Search != null ? "&search=" + Model.Search : "")"
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>
+48
View File
@@ -0,0 +1,48 @@
using AsadiTools.Data;
using AsadiTools.Models;
using AsadiTools.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace AsadiTools.Pages.Shop;
public class ShopIndexModel(AppDbContext db, CartService cart) : PageModel
{
public const int PageSize = 12;
public List<Product> Products { get; private set; } = [];
public string? Category { get; private set; }
public string? Brand { get; private set; }
public string? Search { get; private set; }
public int CurrentPage { get; private set; } = 1;
public int TotalPages { get; private set; }
public int TotalCount { get; private set; }
public async Task OnGetAsync(string? category, string? brand, string? search, int page = 1)
{
Category = category;
Brand = brand;
Search = search;
var q = db.Products.Where(p => p.IsActive);
if (category is not null) q = q.Where(p => p.Category == category);
if (brand is not null) q = q.Where(p => p.Brand == brand);
if (search is not null) q = q.Where(p => p.NameFa.Contains(search) || (p.NameEn != null && p.NameEn.Contains(search)) || (p.Sku != null && p.Sku.Contains(search)));
TotalCount = await q.CountAsync();
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
CurrentPage = Math.Clamp(page, 1, Math.Max(1, TotalPages));
Products = await q.OrderByDescending(p => p.Id)
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
}
public IActionResult OnPostAddToCart(int productId, string nameFa, decimal price, string? sku)
{
cart.AddItem(new CartItem { ProductId = productId, NameFa = nameFa, Price = price, Sku = sku, Qty = 1 });
return RedirectToPage();
}
}
+5
View File
@@ -0,0 +1,5 @@
@using AsadiTools
@using AsadiTools.Models
@using AsadiTools.Services
@namespace AsadiTools.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+3
View File
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}