feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s

- Discount store pages (products, cart, orders, order detail)
- Blog pages and services
- Payment gateway callback page
- AppImage component, EmptyState, LoadingState, PageHeader
- DiscountCartService, DiscountOrderService, DiscountProductService
- BlogCategoryService, BlogPostService, SitePageService, ImageCacheService
- PhoneVerifyForm component
- Profile Hub page
- UI/UX improvements across all pages
- landing.js for homepage
- Config and routing updates
This commit is contained in:
masoodafar-web
2026-02-16 00:51:39 +03:30
parent f0d1156457
commit 6db83ccd90
104 changed files with 7668 additions and 1370 deletions
+240
View File
@@ -0,0 +1,240 @@
# 📋 تاریخچه تغییرات FrontOffice — کارا بازار سلامت
> **آخرین بروزرسانی**: بهمن ۱۴۰۴ (فوریه ۲۰۲۶)
> **فریمورک**: Blazor Server + MudBlazor 8.14 + .NET 9
---
## 🔖 نسخه ۲.۵.۰ — بهمن ۱۴۰۴
### ۱. محافظت صفحات نیازمند احراز هویت (Auth Guard)
**فایل‌های تغییریافته:**
- `Shared/MainLayout.razor.cs`
**شرح:**
قبلاً هیچ محافظتی در سطح مسیریابی وجود نداشت — کاربر غیرلاگین می‌توانست مستقیماً به `/profile/*`، `/commission/*`، `/cart` و... دسترسی پیدا کند.
**تغییرات:**
- متد `EnforceAuthGuardAsync()` اضافه شد — در هر تغییر مسیر و اولین بار رندر اجرا می‌شود
- متد `IsProtectedRoute(path)` مسیرهای محافظت‌شده را تشخیص می‌دهد
- کاربر غیرلاگین → ریدایرکت به `/` (صفحه اصلی)
**مسیرهای محافظت‌شده:**
| گروه | مسیرها |
|---|---|
| پروفایل | `/profile/*` |
| کمیسیون | `/commission/*` |
| شبکه | `/network/*` |
| باشگاه | `/club/*` |
| سبد خرید | `/cart`, `/checkout*`, `/orders`, `/order/*`, `/order-tracking/*` |
| پکیج‌ها | `/my-packages` |
| دروازه | `/my-orders`, `/my-cart` |
| فروشگاه تخفیفی | `/discount-store/cart`, `/discount-store/checkout`, `/discount-store/orders`, `/discount-store/order/*` |
**مسیرهای عمومی:**
`/`, `/register`, `/about`, `/faq`, `/contact`, `/blog/*`, `/packages`, `/package/*`, `/products`, `/product/*`, `/categories`, `/stores`, `/discount-store`, `/discount-store/product/*`
---
### ۲. استخراج کامپوننت PhoneVerifyForm
**فایل‌های جدید:**
- `Shared/PhoneVerifyForm.razor`
- `Shared/PhoneVerifyForm.razor.cs`
**فایل‌های تغییریافته:**
- `Shared/AuthDialog.razor`
- `Shared/AuthDialog.razor.cs`
**شرح:**
فرم تلفن + تایید OTP + کپچا که قبلاً به‌صورت `RenderFragment` با `__builder` مستقیم در `AuthDialog` نوشته شده بود، به کامپوننت مستقل `PhoneVerifyForm` استخراج شد.
**ساختار قبل:**
```
AuthDialog.razor
└── @code { PhoneOrVerifyContent() => __builder => { ... } } ← RenderFragment پیچیده
```
**ساختار بعد:**
```
AuthDialog.razor
└── <PhoneVerifyForm @ref="_phoneVerifyForm" ... /> ← کامپوننت مستقل
PhoneVerifyForm.razor ← مارکاپ فرم
PhoneVerifyForm.razor.cs ← پارامترها و فرم رف‌ها
```
**پارامترهای PhoneVerifyForm:**
| پارامتر | نوع | توضیح |
|---|---|---|
| `CurrentStep` | `AuthStep` | مرحله فعلی (Phone / Verify) |
| `PhoneRequest` | `CreateNewOtpTokenRequest` | مدل فرم تلفن |
| `VerifyRequest` | `VerifyOtpTokenRequest` | مدل فرم تایید |
| `CaptchaCode` | `string?` | کد کپچا نمایش‌داده‌شده |
| `CaptchaInput` / `CaptchaInputChanged` | `string?` + `EventCallback` | ورودی کپچا (two-way) |
| `OnRefreshCaptcha` | `EventCallback` | رفرش کپچا |
| `IsBusy` | `bool` | وضعیت بارگذاری |
| `ErrorMessage` / `InfoMessage` | `string?` | پیام‌های خطا/اطلاع |
| `PhoneNumber` | `string?` | شماره تایید‌شده |
| `ResendRemaining` | `int` | ثانیه تا ارسال مجدد |
| `OnChangePhone` / `OnResendOtp` | `EventCallback` | اکشن‌های تایید |
**نکته مهم — Two-way binding کپچا:**
فیلد کپچا با `Value` + `ValueChanged` بایند شده (نه `@bind-Value`) تا مقدار تایپ‌شده از فرزند به والد برگردد:
```razor
<MudTextField Value="CaptchaInput"
ValueChanged="@((string v) => CaptchaInputChanged.InvokeAsync(v))" ... />
```
**دسترسی به فرم‌ها از والد:**
```csharp
// AuthDialog.razor.cs
var phoneForm = _phoneVerifyForm?.GetPhoneForm();
var verifyForm = _phoneVerifyForm?.GetVerifyForm();
```
---
### ۳. بهبود لایوت مدال ورود (AuthDialog)
**فایل‌های تغییریافته:**
- `Shared/AuthDialog.razor`
- `Utilities/AuthDialogService.cs`
- `wwwroot/css/site.css`
**مشکلات قبلی:**
- دیالوگ روی موبایل `FullScreen` بود → فضای خالی بزرگ بین فرم و دکمه‌ها
- `TitleContent` و `DialogActions` جدا → گپ عمودی
- ردیف کپچا با `MudStack Row="true"` → آیتم‌ها عمودی رندر می‌شدند
**تغییرات:**
1. **حذف FullScreen**: `AuthDialogService` حالا همیشه `MaxWidth.ExtraSmall, FullWidth=true, CloseButton=true`
2. **ادغام محتوا**: آواتار + عنوان + فرم + دکمه‌ها همه داخل `DialogContent` → بدون `TitleContent` و `DialogActions`
3. **کلاس `auth-dialog-wrapper`**: CSS با `.auth-dialog-wrapper .mud-dialog-title { display: none; }` عنوان پیش‌فرض دیالوگ رو مخفی می‌کنه
4. **ردیف کپچا**: از `MudStack Row` به `div.captcha-row` با CSS flex اختصاصی
5. **حالت Inline**: بدون تغییر ساختاری — فقط از `PhoneVerifyForm` استفاده می‌کنه
**CSS جدید:**
```css
.auth-dialog-wrapper .mud-dialog-title { display: none; }
.auth-dialog-wrapper .mud-dialog-content { padding-bottom: 24px !important; }
.auth-dialog-content { max-width: 400px; margin: 0 auto; }
.captcha-row { display: flex; align-items: center; gap: 10px; }
.captcha-row > .mud-input-control { flex: 1 1 0; min-width: 0; }
.captcha-row > .mud-paper { flex: 0 0 auto; }
.captcha-row > .mud-button-root { flex: 0 0 auto; }
```
---
### ۴. صفحه لندینگ — حذف /pricing و اضافه بنر بلاگ
**فایل‌های تغییریافته:**
- `Pages/Index.razor`
- `wwwroot/css/site.css`
**تغییرات:**
1. دکمه هیرو «مشاهده پکیج‌ها» → **«آخرین اخبار»** با لینک `/blog`
2. **بنر جدیدترین مطلب** بین هیرو و «سه گام تا شروع» اضافه شد
- تصویر بندانگشتی + عنوان + خلاصه + آیکون شیشه‌ای
- هاور: `translateY(-2px)` + سایه بزرگ‌تر
**CSS جدید:**
```css
.landing-blog-banner { border: 1px solid var(--mud-palette-divider); transition: ... }
.landing-blog-banner:hover { box-shadow: var(--mud-elevation-4); transform: translateY(-2px); }
.landing-blog-thumb { width: 80px; height: 80px; border-radius: 12px; }
.landing-blog-title { -webkit-line-clamp: 1; font-weight: 600; }
.landing-blog-summary { -webkit-line-clamp: 1; }
```
---
### ۵. افزایش ارتفاع تکست‌باکس‌ها (Global)
**فایل تغییریافته:**
- `wwwroot/css/site.css`
**قبل:** `padding: 10px 14px`
**بعد:** `padding: 14px 14px` + `font-size: 1rem`
```css
.mud-input-outlined .mud-input-slot {
padding: 14px 14px !important;
font-size: 1rem;
}
```
تمام فیلدهای Outlined در کل اپلیکیشن بزرگ‌تر شدند.
---
### ۶. اصلاح RTL فیلدهای ورودی
**فایل تغییریافته:**
- `wwwroot/css/site.css`
**مشکل:** فیلدهای `type="tel"` به‌صورت پیش‌فرض مرورگر `direction: ltr` می‌گیرن — لیبل سمت راست ولی placeholder/cursor سمت چپ.
**اصلاح:**
```css
.mud-input-slot input,
.mud-input-slot textarea {
direction: rtl !important;
text-align: right !important;
}
```
---
### ۷. اصلاح captcha-box CSS
**فایل تغییریافته:**
- `wwwroot/css/site.css`
**قبل:**
```css
.captcha-box {
min-width: 120px; min-height: 56px;
background: linear-gradient(135deg, rgba(123,97,255,.12), rgba(255,140,189,.12));
}
```
**بعد:**
```css
.captcha-box {
min-width: 96px; min-height: 48px;
border-radius: var(--mud-default-borderradius);
background: rgba(99,102,241,.08);
border: 1px solid var(--mud-palette-divider);
}
```
---
## 📁 نقشه فایل‌ها
```
Shared/
├── AuthDialog.razor ← بازنویسی (کامپوننت PhoneVerifyForm جایگزین RenderFragment)
├── AuthDialog.razor.cs ← بروزرسانی (استفاده از _phoneVerifyForm)
├── PhoneVerifyForm.razor ← ✨ جدید (فرم تلفن + تایید + کپچا)
├── PhoneVerifyForm.razor.cs ← ✨ جدید (پارامترها و فرم رف‌ها)
├── MainLayout.razor.cs ← Auth Guard اضافه شد
Pages/
├── Index.razor ← /pricing → /blog + بنر بلاگ
Utilities/
├── AuthDialogService.cs ← حذف FullScreen، ثابت‌سازی سایز
wwwroot/css/
├── site.css ← captcha-row, auth-dialog, RTL fix, input height
```
---
## 🔍 وضعیت بیلد
| تاریخ | خطا | هشدار | توضیح |
|---|---|---|---|
| بهمن ۱۴۰۴ | **۰** | ۱۰۵ | MUD0002 warnings (بی‌خطر — مربوط به MudBlazor analyzer) |
+542
View File
@@ -0,0 +1,542 @@
# 🎨 پلن جامع یکپارچه‌سازی UI/UX — کارا بازار سلامت
> **تاریخ**: بهمن ۱۴۰۴
> **وضعیت**: ✅ **تمام ۶ فاز + فاز ۷ (ناوبری و امنیت) تکمیل شد**
> **هدف**: یکپارچه‌سازی طراحی FrontOffice با ۲۰٪ تغییر UX و ۵۰٪ تغییر UI
> **فریمورک**: Blazor Server + MudBlazor 8.14
> **📋 تاریخچه تغییرات جزئی**: [CHANGELOG.md](CHANGELOG.md)
---
## 📊 خلاصه اجرایی
| شاخص | وضعیت قبل | وضعیت بعد |
|---|---|---|
| **صفحات کل** | ۴۱ صفحه + ۶ کامپوننت مشترک | ۴۱ صفحه + ۸ کامپوننت مشترک |
| **الگوی PageHeader** | ۱۹ صفحه از ۴۱ | ۳۲+ صفحه ✅ |
| **Inline Style سنگین** | ۱۱ صفحه 🔴 | ≤۲ صفحه (فقط gradient‌های تزئینی) ✅ |
| **Loading State** | ۲ الگوی متفاوت (Circular vs Linear) | `<LoadingState>` واحد ✅ |
| **Empty State** | ۴+ الگوی متناقض | `<EmptyState>` واحد ✅ |
| **Container Spacing** | ۶ الگوی متناقض | `py-6` استاندارد ✅ |
| **رنگ‌بندی** | رنگ‌های hardcoded در ۸+ صفحه | CSS Variable ✅ |
| **تم پالت** | Primary `#0380C0` (آبی ساده) | Indigo `#6366f1` + Full PaletteDark ✅ |
| **Elevation** | مخلوط ۲/۳/۴ | استاندارد ۰–۲ ✅ |
| **Build** | ۰ خطا | ۰ خطا ✅ |
### فازها
| فاز | عنوان | وضعیت |
|---|---|---|
| ۱ | زیرساخت دیزاین سیستم | ✅ تکمیل |
| ۲ | صفحات پروفایل | ✅ تکمیل |
| ۳ | صفحات تخصصی | ✅ تکمیل |
| ۴ | بهبود بصری فروشگاه | ✅ تکمیل |
| ۵ | صفحات عمومی | ✅ تکمیل |
| ۶ | پالیش و تست | ✅ تکمیل |
| ۷ | ناوبری، امنیت و بازسازی کامپوننت‌ها | ✅ تکمیل |
---
## 🔍 بخش ۱: تحلیل ضعف‌های جاری
### ۱.۱ ناسازگاری‌های ساختاری (Structural)
#### ❌ ۱.۱.۱ — PageHeader دوگانه
**مشکل**: نیمی از صفحات از `<PageHeader>` استفاده می‌کنند، نیم دیگر header دستی دارند.
| از `<PageHeader>` استفاده می‌کنند ✅ | Header دستی دارند ❌ |
|---|---|
| Store/* (۸ صفحه) | Profile/* (۷ صفحه) |
| DiscountStore/* (۶ صفحه) | Club/* (۲ صفحه) |
| Gateway/* (۳ صفحه) | Commission/* (۲ صفحه) |
| Package/* (۲ صفحه) | Network/* (۱ صفحه) |
| PackageDetail, Checkout | Blog/*, Index, About, Contact, FAQ |
**تأثیر**: ظاهر متفاوت دکمه بازگشت، فاصله‌بندی ناهماهنگ
#### ❌ ۱.۱.۲ — Container MaxWidth متناقض
```
MaxWidth.Large → اکثر صفحات
MaxWidth.Medium → Personal, Settings, OrderTracking, Blog/Post, Gateway/*
MaxWidth.Small → ChangePassword
ترکیبی (loading≠content) → OrderDetail, ProductDetail, PackageDetail
```
**تأثیر**: برخی صفحات پهن‌تر از حد نیاز هستند (مثلاً فرم‌های ساده با Large)
#### ❌ ۱.۱.۳ — Container Padding متناقض
```
py-6 → اکثریت (استاندارد)
py-8 → Package/Packages, Package/MyPackages
pa-2 pa-md-6 → Store/Products, DiscountStore/Products
py-6 py-md-10 → Gateway/*
py-4 py-md-6 → DiscountStore/ProductDetail
py-16 سکشنی → Index, About, Contact, FAQ
```
**قاعده پیشنهادی**: `py-6` برای صفحات داخلی، section-based برای صفحات عمومی
---
### ۱.۲ ناسازگاری‌های بصری (Visual)
#### ❌ ۱.۲.۱ — Inline Style سنگین (۱۱ صفحه)
| صفحه | نمونه مشکل‌دار |
|---|---|
| **Index.razor** | `Style="color:#fff; font-size:clamp(1.6rem,4.5vw,2.4rem);"` |
| **Profile/Index** | `Style="background:rgba(99,102,241,.12);"` |
| **Store/ProductDetail** | `style="width:100%;height:360px;background-image:url(...);"` |
| **PackageDetail** | `Style="background: radial-gradient(600px 280px..."` |
| **Checkout** | `Style="background: radial-gradient(..."` + `Elevation="4"` |
| **Blog/Index** | `Style="color:#fff; font-weight:700;"`, `Style="font-size:3.5rem;"` |
| **Blog/Post** | `Style="width:100%; height:100%;"`, `Style="font-size:4rem;"` |
| **About** | `Style="background: radial-gradient(...);"` |
| **WeeklyBalance** | `Style="background: linear-gradient(135deg, #e8f5e9..."` |
| **Gateway/**** | `Style="background:rgba(16,185,129,.12)..."`, `Style="color:#10b981;"` |
| **DiscountStore/ProductDetail** | `Style="background:rgba(16,185,129,.06)..."` |
#### ❌ ۱.۲.۲ — رنگ‌های Hardcoded
| رنگ | استفاده | باید باشد |
|---|---|---|
| `#10b981` | Gateway (سبز فروشگاه) | `var(--ds-color-store)` |
| `#ef4444` | Gateway (قرمز تخفیفی) | `var(--ds-color-discount)` |
| `#6366f1`, `#818cf8`, `#a78bfa` | Hero/Banner gradients | `var(--ds-gradient-primary)` |
| `rgba(99,102,241,.12)` | Dashboard backgrounds | `var(--ds-primary-soft)` |
| `rgba(16,185,129,.06)` | Discount product highlights | `var(--ds-success-soft)` |
| `#e8f5e9`, `#c8e6c9` | WeeklyBalance stat cards | `var(--ds-success-gradient)` |
#### ❌ ۱.۲.۳ — Elevation ناهماهنگ
```
Elevation="0" → Blog cards (با border)
Elevation="1" → برخی صفحات
Elevation="2" → اکثر صفحات (استاندارد)
Elevation="3" → WeeklyBalance stat cards
Elevation="4" → Checkout.razor
```
**قاعده پیشنهادی**: `Elevation="0"` با `border` = کارت‌های اطلاعاتی، `Elevation="2"` = default
#### ❌ ۱.۲.۴ — Paper Rounding ناهماهنگ
```
rounded-lg (16px) → اکثریت
rounded-xl (20px) → Index, Profile/Index, Gateway, DiscountStore/ProductDetail
بدون class → design system !important → 12px
```
---
### ۱.۳ ناسازگاری‌های UX (تجربه کاربری)
#### ❌ ۱.۳.۱ — Loading State دوگانه
```
MudProgressCircular → Store/*, DiscountStore/*, Package/*, About, Blog, Addresses
MudProgressLinear → Club/*, Commission/*, Network/*, RegisterWizard
```
**مشکل**: کاربر دو تجربه مختلف «در حال بارگذاری» می‌بیند
#### ❌ ۱.۳.۲ — Empty State ناهماهنگ
```
MudAlert Severity.Info → Store/Orders, Categories, DiscountStore/Orders
Icon + Text + Button → Addresses, Package/MyPackages, PackageDetail
MudAlert Severity.Warning → WithdrawalRequests
Custom dashed-border paper → Blog/Index
```
#### ❌ ۱.۳.۳ — Routing Directive ناهماهنگ
```
@attribute [Route(RouteConstants...)] → ۳۹ صفحه ✅
@page "/categories" → Categories.razor ❌
@page "/blog/{Slug}" → Blog/Post.razor ❌
```
#### ❌ ۱.۳.۴ — PackageDetail Loading/Error بدون Container
Loading و Error state در `PackageDetail.razor` بدون `MudContainer` رندر می‌شوند → محتوا تمام‌عرض نمایش می‌یابد.
#### ❌ ۱.۳.۵ — تم فعلی کم‌رنگ
```csharp
// CustomMudTheme.cs فعلی
Primary = "#0380C0" // آبی ساده
// بدون Secondary، Tertiary، Info، Warning تعریف‌شده
// بدون PaletteDark
// بدون LayoutProperties
```
**مشکلات**:
- فقط Primary تعریف شده، بقیه رنگ‌ها default MudBlazor
- Dark mode بدون palette اختصاصی
- بدون `DefaultBorderRadius`، `AppbarHeight` و غیره
- تناقض بین Primary `#0380C0` و gradient‌های CSS با `#6366f1`
---
## 🎯 بخش ۲: معماری دیزاین سیستم هدف
### ۲.۱ سلسله‌مراتب صفحات
```
┌─────────────────────────────────────────────┐
│ MainLayout │
│ ├─ AppBar (fixed, transparent) │
│ ├─ MudMainContent │
│ │ ├─ [Public Pages] → Section-based │
│ │ │ (Index, About, Contact, FAQ, Blog) │
│ │ └─ [Internal Pages] → Container-based │
│ │ ├─ <PageHeader/> │
│ │ ├─ Content (MudStack/MudGrid) │
│ │ └─ </MudContainer> │
│ ├─ Footer (hidden on mobile) │
│ └─ BottomNav (mobile only) │
└─────────────────────────────────────────────┘
```
### ۲.۲ قواعد واحد (Single Source of Truth)
| قاعده | مقدار |
|---|---|
| **Container MaxWidth** | `Large` = لیست/گرید, `Medium` = فرم/جزئیات, `Small` = تک‌فرم ساده |
| **Container Spacing** | `py-6` صفحات داخلی, section-based صفحات عمومی |
| **Paper Elevation** | `0` با border = کارت اطلاعاتی, `2` = default |
| **Paper Rounding** | `rounded-lg` = default, `rounded-xl` = hero/banner |
| **Loading State** | `<LoadingState/>` component واحد |
| **Empty State** | `<EmptyState/>` component واحد |
| **Page Header** | `<PageHeader/>` در تمام صفحات داخلی |
| **Content Wrapper** | `MudStack Spacing="3"` بعد از PageHeader |
### ۲.۳ CSS Variables هدف
```css
:root {
/* ── Brand Colors ── */
--ds-brand-primary: #6366f1; /* Indigo — هویت اصلی */
--ds-brand-secondary: #8b5cf6; /* Purple */
--ds-brand-accent: #a78bfa; /* Light purple */
/* ── Semantic Colors ── */
--ds-color-store: #10b981; /* فروشگاه عادی */
--ds-color-discount: #ef4444; /* فروشگاه تخفیفی */
--ds-color-success: #10b981;
--ds-color-warning: #f59e0b;
--ds-color-error: #ef4444;
--ds-color-info: #3b82f6;
/* ── Soft Backgrounds ── */
--ds-primary-soft: rgba(99,102,241,.08);
--ds-success-soft: rgba(16,185,129,.08);
--ds-error-soft: rgba(239,68,68,.08);
--ds-warning-soft: rgba(245,158,11,.08);
/* ── Gradients ── */
--ds-gradient-primary: linear-gradient(135deg, #6366f1 0%, #818cf8 50%, #a78bfa 100%);
--ds-gradient-store: linear-gradient(135deg, #10b981 0%, #34d399 100%);
--ds-gradient-discount: linear-gradient(135deg, #ef4444 0%, #f97316 50%, #f59e0b 100%);
--ds-gradient-success: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
/* ── Spacing (existing) ── */
--ds-radius-sm: 8px;
--ds-radius-md: 12px;
--ds-radius-lg: 16px;
--ds-radius-xl: 20px;
--ds-transition: 0.2s ease;
--ds-shadow-sm: 0 1px 3px rgba(0,0,0,.06);
--ds-shadow-md: 0 4px 12px rgba(0,0,0,.08);
--ds-shadow-lg: 0 8px 24px rgba(0,0,0,.10);
}
```
---
## 🚀 بخش ۳: فازبندی اجرا
### 🔷 فاز ۱ — زیرساخت دیزاین سیستم (UI ~15%)
> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۶ فایل
| # | تسک | فایل | نوع تغییر |
|---|---|---|---|
| 1.1 | ارتقاء `CustomMudTheme.cs` — اضافه کردن PaletteDark، LayoutProperties، رنگ‌های Secondary/Tertiary/Info، تغییر Primary به `#6366f1` | `CustomMudTheme.cs` | UI |
| 1.2 | توسعه CSS Variables — اضافه کردن brand colors، semantic colors، soft backgrounds، gradients | `site.css` | UI |
| 1.3 | ساخت `<LoadingState>` component واحد | `Shared/LoadingState.razor` (جدید) | UX |
| 1.4 | ساخت `<EmptyState>` component واحد | `Shared/EmptyState.razor` (جدید) | UX |
| 1.5 | بهبود `<PageHeader>` — اضافه کردن آیکون، subtitle اختیاری | `Shared/PageHeader.razor` | UI |
| 1.6 | اضافه کردن `.page-container` CSS pattern | `site.css` | UI |
---
### 🔷 فاز ۲ — یکپارچه‌سازی صفحات Profile (UI ~10%, UX ~5%)
> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۹ فایل
| # | تسک | تغییرات |
|---|---|---|
| 2.1 | `Profile/Personal` → جایگزینی header دستی با `<PageHeader>` | UX |
| 2.2 | `Profile/Addresses``<PageHeader>` + `<LoadingState>` + `<EmptyState>` | UX |
| 2.3 | `Profile/Wallet``<PageHeader>` | UX |
| 2.4 | `Profile/Settings``<PageHeader>` | UX |
| 2.5 | `Profile/Tree``<PageHeader>` | UX |
| 2.6 | `Profile/WithdrawalRequests``<PageHeader>` + `<EmptyState>` | UX |
| 2.7 | `Profile/ChangePassword``<PageHeader>` | UX |
| 2.8 | `Profile/Index` (Dashboard) → حذف inline styles، استفاده از CSS Variables | UI |
| 2.9 | `Club/MembershipPage` + `Club/FeaturesPage``<PageHeader>` + `<LoadingState>` | UX |
---
### 🔷 فاز ۳ — یکپارچه‌سازی صفحات تخصصی (UI ~5%, UX ~5%)
> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۵ فایل
| # | تسک | تغییرات |
|---|---|---|
| 3.1 | `Commission/Dashboard``<PageHeader>` + `<LoadingState>` | UX |
| 3.2 | `Commission/WeeklyBalance``<PageHeader>` + حذف inline gradient styles | UI + UX |
| 3.3 | `Network/NetworkStatistics``<PageHeader>` + `<LoadingState>` | UX |
| 3.4 | `PackageDetail` → wrap loading/error در `MudContainer` | UX bug fix |
| 3.5 | `Checkout` → Elevation=4→2، حذف inline radial-gradient | UI |
---
### 🔷 فاز ۴ — بهبود بصری فروشگاه‌ها (UI ~10%)
> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۶ فایل
| # | تسک | تغییرات |
|---|---|---|
| 4.1 | `Store/Products` → حذف inline hero styles، استفاده از CSS class | UI |
| 4.2 | `Store/ProductDetail` → حذف inline image styles، ساخت `.product-image-main` CSS | UI |
| 4.3 | `DiscountStore/ProductDetail` → حذف hardcoded rgba، استفاده از `--ds-success-soft` | UI |
| 4.4 | `Gateway/*` (۳ صفحه) → حذف hardcoded `#10b981`/`#ef4444`، استفاده از `--ds-color-store`/`--ds-color-discount` | UI |
| 4.5 | یکسان‌سازی Elevation → `0` با border یا `2` | UI |
| 4.6 | یکسان‌سازی Container spacing → `py-6` | UI |
---
### 🔷 فاز ۵ — بهبود صفحات عمومی و بلاگ (UI ~10%)
> **اولویت**: پایین | **ریسک**: پایین | **حجم**: ۷ فایل
| # | تسک | تغییرات |
|---|---|---|
| 5.1 | `Index.razor` → حذف inline styles از hero، استفاده از CSS class | UI |
| 5.2 | `About.razor` → حذف inline radial-gradient | UI |
| 5.3 | `Contact.razor` → cleanup minor inline styles | UI |
| 5.4 | `FAQ.razor` → cleanup minor inline styles | UI |
| 5.5 | `Blog/Index` → حذف inline hero styles، استفاده از CSS class | UI |
| 5.6 | `Blog/Post` → حذف inline styles از hero image و typography | UI |
| 5.7 | `RegisterWizard` → بهینه‌سازی wizard-section dark mode | UI |
---
### 🔷 فاز ۶ — Polish نهایی و فرآیندی (UX ~10%)
> **اولویت**: پایین | **ریسک**: بسیار پایین | **حجم**: ۴ فایل + تست
| # | تسک | تغییرات |
|---|---|---|
| 6.1 | Fix routing inconsistency — Categories + Blog/Post | UX |
| 6.2 | MudSnackbar notifications styling | UI |
| 6.3 | Dialog styling consistency (AuthDialog, AddressDialogs) | UI |
| 6.4 | Micro-interactions — button press, card hover, page transition | UI |
| 6.5 | تست کامل Dark Mode در تمام صفحات | UI + QA |
| 6.6 | تست Mobile Responsive در تمام صفحات | UX + QA |
---
## 📈 بخش ۴: جدول تأثیرگذاری
### تأثیر UI (هدف ~۵۰٪ تغییر)
| حوزه | تعداد فایل | درصد تأثیر |
|---|---|---|
| Theme + CSS Variables | ۲ | ۱۵% (تأثیر سراسری) |
| حذف Inline Styles | ۱۱ | ۱۵% |
| یکسان‌سازی Elevation/Rounding | ۲۰+ | ۱۰% |
| بهبود رنگ‌بندی (CSS Variables) | ۸ | ۵% |
| Micro-interactions | سراسری | ۵% |
| **جمع** | | **~۵۰%** |
### تأثیر UX (هدف ~۲۰٪ تغییر)
| حوزه | تعداد فایل | درصد تأثیر |
|---|---|---|
| PageHeader یکپارچه | ۱۲ صفحه | ۸% |
| LoadingState واحد | ۱۵+ صفحه | ۴% |
| EmptyState واحد | ۸+ صفحه | ۳% |
| Container fixes (PackageDetail) | ۲ | ۲% |
| Routing consistency | ۲ | ۱% |
| Flow improvements | ۲ | ۲% |
| **جمع** | | **~۲۰%** |
---
## 🔧 بخش ۵: مشخصات فنی کامپوننت‌های جدید
### ۵.۱ LoadingState Component
```razor
@* Shared/LoadingState.razor *@
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
@if (!string.IsNullOrWhiteSpace(Message))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">@Message</MudText>
}
</MudStack>
@code {
[Parameter] public string Message { get; set; } = "در حال بارگذاری...";
}
```
### ۵.۲ EmptyState Component
```razor
@* Shared/EmptyState.razor *@
<MudStack AlignItems="AlignItems.Center" Class="py-12" Spacing="3">
<MudIcon Icon="@Icon" Size="Size.Large" Color="Color.Default" Class="mud-text-disabled" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">@Title</MudText>
@if (!string.IsNullOrWhiteSpace(Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary" Style="max-width:400px; text-align:center;">
@Description
</MudText>
}
@if (!string.IsNullOrWhiteSpace(ActionText))
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Href="@ActionHref" OnClick="@OnAction" Class="mt-2">
@ActionText
</MudButton>
}
</MudStack>
@code {
[Parameter] public string Icon { get; set; } = Icons.Material.Filled.Inbox;
[Parameter] public string Title { get; set; } = "موردی یافت نشد";
[Parameter] public string? Description { get; set; }
[Parameter] public string? ActionText { get; set; }
[Parameter] public string? ActionHref { get; set; }
[Parameter] public EventCallback OnAction { get; set; }
}
```
### ۵.۳ PageHeader ارتقاء‌یافته
```razor
@* Shared/PageHeader.razor — ارتقاء‌یافته *@
<div class="page-header">
<MudStack Spacing="0">
<MudText Typo="Typo.h5">@Title</MudText>
@if (!string.IsNullOrWhiteSpace(Subtitle))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">@Subtitle</MudText>
}
</MudStack>
@if (!string.IsNullOrWhiteSpace(BackHref))
{
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@BackHref">بازگشت</MudButton>
}
else
{
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack">بازگشت</MudButton>
}
</div>
@code {
[Parameter] public string Title { get; set; } = "";
[Parameter] public string? Subtitle { get; set; }
[Parameter] public string? BackHref { get; set; }
[Inject] private IJSRuntime JS { get; set; } = default!;
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
```
---
## ✅ بخش ۶: چک‌لیست تکمیل هر فاز
### فاز ۱ چک‌لیست:
- [ ] `CustomMudTheme.cs` — Primary→`#6366f1`, PaletteDark اضافه شد
- [ ] `site.css` — CSS Variables جدید (brand, semantic, soft, gradient)
- [ ] `Shared/LoadingState.razor` — ساخته و تست شد
- [ ] `Shared/EmptyState.razor` — ساخته و تست شد
- [ ] `Shared/PageHeader.razor` — Subtitle parameter اضافه شد
- [ ] `.page-container` CSS pattern اضافه شد
- [ ] Build: 0 errors ✅
- [ ] Dark Mode: صحیح ✅
- [ ] Mobile: صحیح ✅
### فاز ۲ چک‌لیست:
- [ ] `Profile/Personal``<PageHeader>`
- [ ] `Profile/Addresses``<PageHeader>` + `<LoadingState>` + `<EmptyState>`
- [ ] `Profile/Wallet``<PageHeader>`
- [ ] `Profile/Settings``<PageHeader>`
- [ ] `Profile/Tree``<PageHeader>`
- [ ] `Profile/WithdrawalRequests``<PageHeader>` + `<EmptyState>`
- [ ] `Profile/ChangePassword``<PageHeader>`
- [ ] `Profile/Index` → inline styles → CSS
- [ ] `Club/*``<PageHeader>` + `<LoadingState>`
- [ ] Build: 0 errors ✅
### فاز ۳ چک‌لیست:
- [ ] `Commission/*``<PageHeader>` + `<LoadingState>`
- [ ] `Network/*``<PageHeader>` + `<LoadingState>`
- [ ] `PackageDetail` → MudContainer wrapper برای loading/error
- [ ] `Checkout` → Elevation fix + inline cleanup
- [ ] `WeeklyBalance` → inline gradient → CSS class
- [ ] Build: 0 errors ✅
### فاز ۴ چک‌لیست:
- [ ] `Store/Products` → hero inline → CSS
- [ ] `Store/ProductDetail` → image inline → CSS class
- [ ] `DiscountStore/ProductDetail` → rgba → variable
- [ ] `Gateway/*` → hardcoded → variable
- [ ] Elevation یکسان‌سازی
- [ ] Container spacing یکسان‌سازی
- [ ] Build: 0 errors ✅
### فاز ۵ چک‌لیست:
- [ ] `Index.razor` → hero inline cleanup
- [ ] `About.razor` → radial-gradient cleanup
- [ ] `Blog/Index` + `Blog/Post` → inline cleanup
- [ ] `Contact.razor` + `FAQ.razor` → minor cleanup
- [ ] Build: 0 errors ✅
### فاز ۶ چک‌لیست:
- [ ] Routing fix (Categories, Blog/Post)
- [ ] MudSnackbar styling
- [ ] Dialog consistency
- [ ] Dark mode full test
- [ ] Mobile responsive full test
- [ ] Build: 0 errors ✅
---
## 📋 بخش ۷: خلاصه تغییرات در یک نگاه
```
فایل‌های تغییر‌یافته:
├── CustomMudTheme.cs [فاز ۱] — ارتقاء کامل تم
├── site.css [فاز ۱-۵] — CSS Variables + class‌های جدید
├── Shared/LoadingState.razor [فاز ۱] — جدید
├── Shared/EmptyState.razor [فاز ۱] — جدید
├── Shared/PageHeader.razor [فاز ۱] — بهبود
├── Profile/* (۷ فایل) [فاز ۲] — PageHeader + LoadingState
├── Profile/Index.razor [فاز ۲] — حذف inline styles
├── Club/* (۲ فایل) [فاز ۲] — PageHeader + LoadingState
├── Commission/* (۲ فایل) [فاز ۳] — PageHeader + LoadingState + cleanup
├── Network/* (۱ فایل) [فاز ۳] — PageHeader + LoadingState
├── PackageDetail.razor [فاز ۳] — Container fix
├── Checkout.razor [فاز ۳] — Elevation + cleanup
├── Store/* (۲ فایل) [فاز ۴] — inline → CSS
├── DiscountStore/ProductDetail [فاز ۴] — rgba → variable
├── Gateway/* (۳ فایل) [فاز ۴] — hardcoded → variable
├── Index.razor [فاز ۵] — hero cleanup
├── About.razor [فاز ۵] — gradient cleanup
├── Blog/* (۲ فایل) [فاز ۵] — inline cleanup
├── Contact.razor + FAQ.razor [فاز ۵] — minor cleanup
└── Categories + Blog/Post [فاز ۶] — routing fix
```
**مجموع فایل‌های تأثیرپذیر**: ~۳۵ فایل
**فایل‌های جدید**: ۲ (LoadingState, EmptyState)
**میزان تغییر UI**: ~۵۰٪
**میزان تغییر UX**: ~۲۰٪
**ریسک شکست**: پایین (تغییرات تدریجی، build verification در هر فاز)
+28
View File
@@ -21,6 +21,13 @@ using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership; using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Commission; using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.AppVersion; using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
namespace Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection;
@@ -66,6 +73,16 @@ public static class ConfigureServices
services.AddScoped<AppVersionService>(); services.AddScoped<AppVersionService>();
// Main Service for core functionality // Main Service for core functionality
services.AddScoped<MainService>(); services.AddScoped<MainService>();
// Site Page service for dynamic content (About, Contact)
services.AddScoped<SitePageService>();
// Blog Post service for landing page latest posts & blog pages
services.AddScoped<BlogPostService>();
// Blog Category service for blog sidebar filters
services.AddScoped<BlogCategoryService>();
// Discount Store services
services.AddScoped<DiscountProductService>();
services.AddScoped<DiscountCartService>();
services.AddScoped<DiscountOrderService>();
// Device detection: very light, dependency-free // Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>(); services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only) // PDF generation (Chromium only)
@@ -115,6 +132,17 @@ public static class ConfigureServices
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.NetworkMembership.NetworkMembershipContract.NetworkMembershipContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.NetworkMembership.NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Commission.CommissionContract.CommissionContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Commission.CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.AppVersion.AppVersionContract.AppVersionContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.AppVersion.AppVersionContract.AppVersionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient>);
// Image Resolver Service
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ImageResolver.ImageResolverContract.ImageResolverContractClient>);
services.AddScoped<ImageCacheService>();
// Discount Store gRPC clients
services.AddScoped(CreateAuthenticatedClient<DiscountProductContract.DiscountProductContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountCategoryContract.DiscountCategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShoppingCartContract.DiscountShoppingCartContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountOrderContract.DiscountOrderContractClient>);
return services; return services;
} }
+261 -173
View File
@@ -1,192 +1,280 @@
@attribute [Route(RouteConstants.About.Index)] @attribute [Route(RouteConstants.About.Index)]
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject SitePageService SitePageService
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle> <PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
<!-- Hero Section --> @if (_loading)
<section class="about-hero-section py-16"> {
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<MudGrid Justify="Justify.Center" Spacing="4"> <LoadingState />
<MudItem xs="12" md="6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudStack Spacing="3">
<MudText Typo="Typo.h2" Class="mb-3">
پلتفرم هوشمند تیم‌سازی و مدیریت فروش
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.
</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="ScrollToContact">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="images/team-work.jpg"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer> </MudContainer>
</section> }
else
<MudStack Spacing="8"> {
<!-- Hero Section -->
<!-- Mission & Vision --> <section class="about-hero-section py-16">
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText> <MudGrid Justify="Justify.Center" Spacing="4">
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Elevation="3" Class="pa-6 text-center h-100"> <MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" /> <MudStack Spacing="3">
<MudText Typo="Typo.h5" Class="mb-3">چشم‌انداز</MudText> <MudText Typo="Typo.h2" Class="mb-3">
<MudText Typo="Typo.body1" Class="mud-text-secondary"> @(_page?.HeroTitle ?? "پلتفرم هوشمند تیم‌سازی و مدیریت فروش")
تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
</MudText> </MudText>
</MudPaper> <MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
@(_page?.HeroSubtitle ?? "ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.")
</MudText>
<MudStack Row="true" Spacing="2" Class="flex-wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Elevation="3" Class="pa-6 text-center h-100"> <MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" /> <AppImage Path="@(_page?.HeroImagePath ?? "images/team-work.jpg")"
<MudText Typo="Typo.h5" Class="mb-3">مأموریت</MudText> Alt="جلسه تیم فروش"
<MudText Typo="Typo.body1" Class="mud-text-secondary"> ObjectFit="ObjectFit.Cover"
توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار. ObjectPosition="ObjectPosition.Center"
</MudText> Style="width:100%"
Class="rounded-xl" />
</MudPaper> </MudPaper>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
</MudContainer> </MudContainer>
</section> </section>
<!-- Company Values --> <MudStack Spacing="8">
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- Team Section --> <!-- Mission & Vision -->
<section class="pb-12 bg-grey-50"> <section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText> <MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center"> <MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" md="6">
<MudPaper Elevation="3" Class="pa-4 text-center"> <MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudAvatar Size="Size.Large" Class="mb-3"> <MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudImage ObjectFit="ObjectFit.Cover" <MudText Typo="Typo.h5" Class="mb-3">
ObjectPosition="ObjectPosition.Center" @(_visionSection?.Title ?? "چشم‌انداز")
Src="images/avatar3.jpg" /> </MudText>
</MudAvatar> @if (!string.IsNullOrWhiteSpace(_visionSection?.HtmlContent))
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText> {
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText> <MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudText Typo="Typo.caption" Class="mud-text-secondary"> @((MarkupString)_visionSection.HtmlContent)
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار </MudText>
</MudText> }
</MudPaper> else
</MudItem> {
<MudItem xs="12" sm="6" md="4"> <MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudPaper Elevation="3" Class="pa-4 text-center"> تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
<MudAvatar Size="Size.Large" Class="mb-3"> </MudText>
<MudImage ObjectFit="ObjectFit.Cover" }
ObjectPosition="ObjectPosition.Center" </MudPaper>
Src="images/avatar4.jpg" /> </MudItem>
</MudAvatar> <MudItem xs="12" md="6">
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText> <MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText> <MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
<MudText Typo="Typo.caption" Class="mud-text-secondary"> <MudText Typo="Typo.h5" Class="mb-3">
متخصص در طراحی تجربه کاربری و توسعه محصول @(_missionSection?.Title ?? "مأموریت")
</MudText> </MudText>
</MudPaper> @if (!string.IsNullOrWhiteSpace(_missionSection?.HtmlContent))
</MudItem> {
<MudItem xs="12" sm="6" md="4"> <MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudPaper Elevation="3" Class="pa-4 text-center"> @((MarkupString)_missionSection.HtmlContent)
<MudAvatar Size="Size.Large" Class="mb-3"> </MudText>
<MudImage ObjectFit="ObjectFit.Cover" }
ObjectPosition="ObjectPosition.Center" else
Src="images/avatar5.jpg" /> {
</MudAvatar> <MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText> توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار.
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText> </MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary"> }
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده </MudPaper>
</MudText> </MudItem>
</MudPaper> </MudGrid>
</MudItem> </MudContainer>
</MudGrid> </section>
</MudContainer>
</section> <!-- Company Values -->
</MudStack> <section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@if (_valueSections.Any())
{
@foreach (var value in _valueSections)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
@if (!string.IsNullOrWhiteSpace(value.IconName))
{
<MudIcon Icon="@value.IconName" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
}
<MudText Typo="Typo.h6" Class="mb-2">@value.Title</MudText>
@if (!string.IsNullOrWhiteSpace(value.HtmlContent))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@((MarkupString)value.HtmlContent)
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded values *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<!-- Team Section -->
<section class="pb-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
@if (_teamSections.Any())
{
@foreach (var member in _teamSections)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
@if (!string.IsNullOrWhiteSpace(member.ImagePath))
{
<AppImage Path="@member.ImagePath"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Person" />
}
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@member.Title</MudText>
@if (!string.IsNullOrWhiteSpace(member.Subtitle))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@member.Subtitle</MudText>
}
@if (!string.IsNullOrWhiteSpace(member.HtmlContent))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@((MarkupString)member.HtmlContent)
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded team *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar3.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar4.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
متخصص در طراحی تجربه کاربری و توسعه محصول
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar5.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
</MudStack>
}
+31 -2
View File
@@ -1,8 +1,37 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages; namespace FrontOffice.Main.Pages;
public partial class About public partial class About
{ {
private void ScrollToContact() private bool _loading = true;
private SitePageDto? _page;
private SitePageSectionDto? _visionSection;
private SitePageSectionDto? _missionSection;
private List<SitePageSectionDto> _valueSections = new();
private List<SitePageSectionDto> _teamSections = new();
protected override async Task OnInitializedAsync()
{ {
// TODO: Implement smooth scroll to contact section try
{
_page = await SitePageService.GetByKeyAsync("about");
if (_page != null)
{
_visionSection = _page.GetSection("vision");
_missionSection = _page.GetSection("mission");
_valueSections = _page.GetSections("value-").ToList();
_teamSections = _page.GetSections("team-").ToList();
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
} }
} }
+172
View File
@@ -0,0 +1,172 @@
@attribute [Route(RouteConstants.Blog.Index)]
<PageTitle>بلاگ | کارا بازار سلامت</PageTitle>
@* ═══════════════════════════════════════════════
BLOG HEADER
═══════════════════════════════════════════════ *@
<section class="blog-hero">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center">
<MudText Typo="Typo.h3" Class="dash-hero-name">بلاگ</MudText>
<MudText Typo="Typo.body1" Class="dash-hero-sub mt-2">
آخرین مطالب، آموزش‌ها و اخبار کارا بازار سلامت
</MudText>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
SEARCH + FILTERS
═══════════════════════════════════════════════ *@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudPaper Elevation="0" Class="pa-4 rounded-xl mb-6" Style="border:1px solid var(--mud-palette-divider);">
<MudGrid Spacing="3" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="5">
<MudTextField @bind-Value="_searchTerm"
Placeholder="جستجو در مقالات..."
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="false"
OnKeyUp="OnSearchKeyUp"
Class="rounded-lg" />
</MudItem>
<MudItem xs="12" sm="6" md="5">
<MudSelect T="long?" Value="_selectedCategoryId"
Placeholder="همه دسته‌بندی‌ها"
Variant="Variant.Outlined"
Class="rounded-lg"
Clearable="true"
ValueChanged="OnCategoryChanged">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@cat.Id">
@cat.Title
@if (cat.PostCount > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mr-2" Style="display:inline;">(@cat.PostCount)</MudText>
}
</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Class="rounded-lg"
StartIcon="@Icons.Material.Filled.Search"
OnClick="SearchPosts">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@* ═══════════════════════════════════════════════
BLOG POSTS GRID
═══════════════════════════════════════════════ *@
@if (_isLoading)
{
<LoadingState />
}
else if (!_posts.Any())
{
<MudPaper Elevation="0" Class="pa-12 text-center rounded-xl" Style="border:1px dashed var(--mud-palette-divider);">
<MudIcon Icon="@Icons.Material.Outlined.Article" Size="Size.Large" Class="mud-text-secondary mb-3" Style="font-size:3.5rem;" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">مقاله‌ای یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-1">
@if (!string.IsNullOrWhiteSpace(_searchTerm) || _selectedCategoryId.HasValue)
{
<span>فیلترها را تغییر دهید یا عبارت دیگری جستجو کنید.</span>
}
else
{
<span>به‌زودی مقالات جدید منتشر خواهد شد.</span>
}
</MudText>
</MudPaper>
}
else
{
<MudGrid Spacing="4">
@foreach (var post in _posts)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
@* Thumbnail *@
<div class="blog-thumb" style="height:200px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:200px;" />
}
else
{
<div style="height:200px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.4);" />
</div>
}
</div>
@* Content *@
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1 blog-title-clamp">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3 blog-summary-clamp">
@post.Summary
</MudText>
}
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-auto">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
@post.ViewCount.ToString("N0")
</MudText>
</MudStack>
</div>
</MudPaper>
</MudItem>
}
</MudGrid>
@* ── Pagination ── *@
@if (_totalPages > 1)
{
<div class="d-flex justify-center mt-8">
<MudPagination Count="@_totalPages"
Selected="@_currentPage"
SelectedChanged="OnPageChanged"
Color="Color.Primary"
Variant="Variant.Outlined"
Class="rounded-lg" />
</div>
}
}
</MudContainer>
@@ -0,0 +1,91 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace FrontOffice.Main.Pages.Blog;
public partial class Index
{
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private BlogCategoryService BlogCategoryService { get; set; } = default!;
// ── State ──
private List<BlogPostCardDto> _posts = new();
private List<BlogCategoryDto> _categories = new();
private string? _searchTerm;
private long? _selectedCategoryId;
private int _currentPage = 1;
private int _totalPages;
private bool _isLoading = true;
private const int PageSize = 9;
protected override async Task OnInitializedAsync()
{
// Load categories and posts in parallel
var categoriesTask = BlogCategoryService.GetActiveCategoriesAsync();
var postsTask = LoadPostsAsync();
_categories = await categoriesTask;
await postsTask;
}
private async Task LoadPostsAsync()
{
_isLoading = true;
StateHasChanged();
try
{
var result = await BlogPostService.GetPublishedPostsAsync(
page: _currentPage,
pageSize: PageSize,
search: _searchTerm,
categoryId: _selectedCategoryId);
_posts = result.Posts;
_totalPages = result.TotalPages;
}
catch
{
_posts = new();
_totalPages = 0;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task SearchPosts()
{
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SearchPosts();
}
}
private async Task OnCategoryChanged(long? categoryId)
{
_selectedCategoryId = categoryId;
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadPostsAsync();
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
}
+122
View File
@@ -0,0 +1,122 @@
@page "/blog/{Slug}"
<PageTitle>@(_post?.Title ?? "مقاله") | بلاگ</PageTitle>
@if (_isLoading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<LoadingState />
</MudContainer>
}
else if (_post == null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="مقاله یافت نشد"
Description="این مقاله حذف شده یا آدرس اشتباه است."
ActionText="بازگشت به بلاگ"
ActionHref="/blog" />
</MudContainer>
}
else
{
@* ═══ Featured Image Header ═══ *@
@if (!string.IsNullOrWhiteSpace(_post.FeaturedImagePath))
{
<div class="blog-detail-hero">
<AppImage Path="@_post.FeaturedImagePath"
Alt="@_post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:100%;" />
<div class="blog-detail-hero-overlay"></div>
</div>
}
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
@* ── Breadcrumb ── *@
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-4">
<MudLink Href="/" Typo="Typo.caption" Class="mud-text-secondary">خانه</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudLink Href="/blog" Typo="Typo.caption" Class="mud-text-secondary">بلاگ</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_post.Title</MudText>
</MudStack>
@* ── Article Header ── *@
<MudPaper Elevation="0" Class="pa-0 mb-6">
@if (_post.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="mb-3 flex-wrap">
@foreach (var cat in _post.Categories)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary" Class="rounded-pill"
OnClick="() => NavigateToCategoryFilter(cat.Id)">
@cat.Title
</MudChip>
}
</MudStack>
}
<MudText Typo="Typo.h4" Class="mb-3 fw-bold" Style="line-height:1.6;">
@_post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(_post.Summary))
{
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-4" Style="line-height:1.8;">
@_post.Summary
</MudText>
}
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="mb-4 flex-wrap">
@if (_post.PublishedAt.HasValue)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
</MudStack>
}
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.ViewCount.ToString("N0") بازدید
</MudText>
</MudStack>
</MudStack>
<MudDivider Class="mb-6" />
</MudPaper>
@* ── Article Content ── *@
<MudPaper Elevation="0" Class="blog-content mb-8">
@((MarkupString)_post.HtmlContent)
</MudPaper>
@* ── Tags ── *@
@if (_post.Tags.Any())
{
<MudDivider Class="mb-4" />
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-6 flex-wrap">
<MudIcon Icon="@Icons.Material.Filled.LocalOffer" Size="Size.Small" Class="mud-text-secondary ml-2" />
@foreach (var tag in _post.Tags)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Class="rounded-pill">
@tag.Title
</MudChip>
}
</MudStack>
}
@* ── Back to Blog ── *@
<div class="text-center mt-6 mb-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="GoBack"
Class="rounded-pill" StartIcon="@Icons.Material.Filled.ArrowForward">
بازگشت به بلاگ
</MudButton>
</div>
</MudContainer>
}
@@ -0,0 +1,51 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace FrontOffice.Main.Pages.Blog;
public partial class Post
{
[Inject] private IJSRuntime JS { get; set; } = default!;
[Parameter] public string Slug { get; set; } = string.Empty;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private BlogPostDetailDto? _post;
private bool _isLoading = true;
protected override async Task OnParametersSetAsync()
{
if (string.IsNullOrWhiteSpace(Slug)) return;
_isLoading = true;
StateHasChanged();
try
{
_post = await BlogPostService.GetBySlugAsync(Slug);
// Fire-and-forget: increment view count
if (_post != null)
{
_ = BlogPostService.IncrementViewCountAsync(_post.Id);
}
}
catch
{
_post = null;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private void NavigateToCategoryFilter(long categoryId)
{
Navigation.NavigateTo($"/blog?category={categoryId}");
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
+8 -7
View File
@@ -5,10 +5,11 @@
<PageTitle>تکمیل خرید</PageTitle> <PageTitle>تکمیل خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تکمیل خرید" BackHref="@RouteConstants.Package.List" />
<MudGrid Spacing="4"> <MudGrid Spacing="4">
<!-- Header --> <!-- Header -->
<MudItem xs="12"> <MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" /> <MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
@@ -23,7 +24,7 @@
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
<MudStack> <MudStack>
<!-- Package Details --> <!-- Package Details -->
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText> <MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
@if (_selectedPackage != null) @if (_selectedPackage != null)
@@ -33,9 +34,9 @@
<MudGrid Spacing="3"> <MudGrid Spacing="3">
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);"> <MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_selectedPackage.Image" <AppImage Path="@_selectedPackage.Image"
Alt="@_selectedPackage.Title" Alt="@_selectedPackage.Title"
Height="150" ImgHeight="150"
ObjectFit="ObjectFit.Cover" ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" ObjectPosition="ObjectPosition.Center"
Style="width:100%" Style="width:100%"
@@ -67,7 +68,7 @@
</MudPaper> </MudPaper>
<!-- Address Selection --> <!-- Address Selection -->
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText> <MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
@if (_isLoadingAddresses) @if (_isLoadingAddresses)
@@ -149,7 +150,7 @@
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudStack Spacing="4"> <MudStack Spacing="4">
<!-- Discount Code Section --> <!-- Discount Code Section -->
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">کد تخفیف</MudText> <MudText Typo="Typo.h5" Class="mb-4">کد تخفیف</MudText>
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudTextField @bind-Value="_discountCode" <MudTextField @bind-Value="_discountCode"
@@ -176,7 +177,7 @@
<!-- Order Summary --> <!-- Order Summary -->
@if (_selectedPackage != null) @if (_selectedPackage != null)
{ {
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">خلاصه سفارش</MudText> <MudText Typo="Typo.h5" Class="mb-4">خلاصه سفارش</MudText>
<MudStack Spacing="3"> <MudStack Spacing="3">
+1 -1
View File
@@ -59,7 +59,7 @@ public partial class Checkout
Id: response.Id, Id: response.Id,
Title: response.Title, Title: response.Title,
Body: response.Description, Body: response.Description,
Image: UrlUtility.DownloadUrl + response.ImagePath, Image: response.ImagePath ?? string.Empty,
Price: response.Price Price: response.Price
); );
_finalPrice = _selectedPackage.Price; _finalPrice = _selectedPackage.Price;
@@ -3,15 +3,13 @@
@using MudBlazor @using MudBlazor
@inject ClubConfigurationService ClubConfigService @inject ClubConfigurationService ClubConfigService
@inject IDialogService DialogService @inject IDialogService DialogService
@inject IJSRuntime JS
<PageTitle>ویژگی‌های باشگاه مشتریان</PageTitle> <PageTitle>ویژگی‌های باشگاه مشتریان</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="ویژگی‌های باشگاه مشتریان" BackHref="@RouteConstants.Club.Membership" />
<MudText Typo="Typo.h5">ویژگی‌های باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Club.Membership">بازگشت</MudButton>
</MudStack>
<!-- توضیحات کلی باشگاه --> <!-- توضیحات کلی باشگاه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -30,13 +28,13 @@
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState Message="در حال دریافت ویژگی‌ها..." />
} }
else if (_features == null || !_features.Any()) else if (_features == null || !_features.Any())
{ {
<MudAlert Severity="Severity.Info"> <EmptyState Icon="@Icons.Material.Filled.Stars"
هنوز ویژگی‌ای برای شما فعال نشده است. Title="هنوز ویژگی‌ای برای شما فعال نشده است"
</MudAlert> Description="با خرید پکیج یا ارتقاء عضویت، ویژگی‌های اختصاصی فعال می‌شوند." />
} }
else else
{ {
@@ -73,7 +71,7 @@
FullWidth="true" FullWidth="true"
Size="Size.Large" Size="Size.Large"
StartIcon="@Icons.Material.Filled.ArrowBack" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@RouteConstants.Club.Membership"> OnClick="GoBack">
بازگشت به صفحه باشگاه بازگشت به صفحه باشگاه
</MudButton> </MudButton>
</MudStack> </MudStack>
@@ -190,4 +188,6 @@
_selectedFeature = feature; _selectedFeature = feature;
_showDetailDialog = true; _showDetailDialog = true;
} }
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
} }
@@ -7,14 +7,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">عضویت باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState Message="در حال دریافت اطلاعات عضویت..." />
} }
else if (_membership is not null) else if (_membership is not null)
{ {
@@ -8,10 +8,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">پاداش‌های من</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<!-- فیلترها --> <!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg"> <MudPaper Elevation="2" Class="pa-3 rounded-lg">
@@ -52,7 +49,7 @@
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState Message="در حال دریافت پاداش‌ها..." />
} }
else else
{ {
@@ -7,10 +7,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="گزارش هفتگی پاداش" BackHref="@RouteConstants.Commission.Dashboard" />
<MudText Typo="Typo.h5">گزارش هفتگی پاداش</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Commission.Dashboard">بازگشت</MudButton>
</MudStack>
<!-- انتخاب هفته --> <!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg"> <MudPaper Elevation="2" Class="pa-3 rounded-lg">
@@ -50,7 +47,7 @@
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState Message="در حال دریافت گزارش..." />
} }
else if (_weeklyBalance is not null) else if (_weeklyBalance is not null)
{ {
@@ -76,11 +73,11 @@
<MudGrid Spacing="2"> <MudGrid Spacing="2">
<MudItem xs="6"> <MudItem xs="6">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);"> <MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-success-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center"> <MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" /> <MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Color="Color.Success">تیم دوم</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Success">تیم دوم</MudText>
<MudText Typo="Typo.h5" Color="Color.Success" Style="font-weight: bold;">@_weeklyBalance.RightBalanceFormatted</MudText> <MudText Typo="Typo.h5" Color="Color.Success" Class="dash-stat-value">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Success" <MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()" Value="@GetRightPercentage()"
Size="Size.Medium" Size="Size.Medium"
@@ -93,11 +90,11 @@
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="6"> <MudItem xs="6">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);"> <MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-info-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center"> <MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large"/> <MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large"/>
<MudText Typo="Typo.subtitle2" Color="Color.Info">تیم اول</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Info">تیم اول</MudText>
<MudText Typo="Typo.h5" Color="Color.Info" Style="font-weight: bold;">@_weeklyBalance.LeftBalanceFormatted</MudText> <MudText Typo="Typo.h5" Color="Color.Info" Class="dash-stat-value">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Info" <MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()" Value="@GetLeftPercentage()"
Size="Size.Medium" Size="Size.Medium"
@@ -114,7 +111,7 @@
<!-- محاسبات پاداش - فشرده‌تر --> <!-- محاسبات پاداش - فشرده‌تر -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-2" Style="font-weight: bold;">محاسبات پاداش</MudText> <MudText Typo="Typo.subtitle1" Class="mb-2 fw-bold">محاسبات پاداش</MudText>
<MudSimpleTable Dense="true" Hover="true" Style="background: transparent;"> <MudSimpleTable Dense="true" Hover="true" Style="background: transparent;">
<tbody> <tbody>
<tr> <tr>
+23 -23
View File
@@ -1,4 +1,5 @@
@attribute [Route(RouteConstants.Contact.Index)] @attribute [Route(RouteConstants.Contact.Index)]
@inject SitePageService SitePageService
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle> <PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
@@ -8,10 +9,10 @@
<MudStack AlignItems="AlignItems.Center" Spacing="4"> <MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ارتباط با ما</MudChip> <MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ارتباط با ما</MudChip>
<MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3"> <MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3">
آماده شنیدن صدای شما هستیم @(_page?.HeroTitle ?? "آماده شنیدن صدای شما هستیم")
</MudText> </MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px"> <MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px">
سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست. @(_page?.HeroSubtitle ?? "سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.")
</MudText> </MudText>
</MudStack> </MudStack>
</MudContainer> </MudContainer>
@@ -25,7 +26,7 @@
<MudGrid Spacing="6"> <MudGrid Spacing="6">
<!-- Contact Form --> <!-- Contact Form -->
<MudItem xs="12" lg="8"> <MudItem xs="12" lg="8">
<MudPaper Elevation="3" Class="pa-6"> <MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-6">فرم تماس</MudText> <MudText Typo="Typo.h5" Class="mb-6">فرم تماس</MudText>
<MudForm @ref="_form" Model="_contactForm"> <MudForm @ref="_form" Model="_contactForm">
@@ -72,12 +73,12 @@
Variant="Variant.Outlined" Variant="Variant.Outlined"
Required="true" Required="true"
RequiredError="انتخاب موضوع الزامی است."> RequiredError="انتخاب موضوع الزامی است.">
<MudSelectItem Value="@("general")">عمومی</MudSelectItem> <MudSelectItem T="string" Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem Value="@("support")">پشتیبانی فنی</MudSelectItem> <MudSelectItem T="string" Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem> <MudSelectItem T="string" Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem Value="@("partnership")">شریک تجاری</MudSelectItem> <MudSelectItem T="string" Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem Value="@("complaint")">شکایت</MudSelectItem> <MudSelectItem T="string" Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem Value="@("suggestion")">پیشنهاد</MudSelectItem> <MudSelectItem T="string" Value="@("suggestion")">پیشنهاد</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
@@ -123,7 +124,7 @@
<MudItem xs="12" lg="4"> <MudItem xs="12" lg="4">
<MudStack Spacing="4"> <MudStack Spacing="4">
<!-- Contact Details --> <!-- Contact Details -->
<MudPaper Elevation="3" Class="pa-6"> <MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">اطلاعات تماس</MudText> <MudText Typo="Typo.h6" Class="mb-4">اطلاعات تماس</MudText>
<MudStack Spacing="4"> <MudStack Spacing="4">
@@ -132,7 +133,7 @@
<div> <div>
<MudText Typo="Typo.body2" >آدرس</MudText> <MudText Typo="Typo.body2" >آدرس</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary"> <MudText Typo="Typo.caption" Class="mud-text-secondary">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶ @(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText> </MudText>
</div> </div>
</MudStack> </MudStack>
@@ -141,7 +142,7 @@
<MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" /> <MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" />
<div> <div>
<MudText Typo="Typo.body2" >تلفن</MudText> <MudText Typo="Typo.body2" >تلفن</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">026-34233563</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactPhone ?? "026-34233563")</MudText>
</div> </div>
</MudStack> </MudStack>
@@ -149,7 +150,7 @@
<MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" /> <MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" />
<div> <div>
<MudText Typo="Typo.body2" >ایمیل</MudText> <MudText Typo="Typo.body2" >ایمیل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">info@kbs1.co</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactEmail ?? "info@kbs1.co")</MudText>
</div> </div>
</MudStack> </MudStack>
@@ -158,8 +159,7 @@
<div> <div>
<MudText Typo="Typo.body2" >ساعات کاری</MudText> <MudText Typo="Typo.body2" >ساعات کاری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary"> <MudText Typo="Typo.caption" Class="mud-text-secondary">
شنبه تا پنج‌شنبه<br /> @((MarkupString)(_contactHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
۹ صبح تا ۶ عصر
</MudText> </MudText>
</div> </div>
</MudStack> </MudStack>
@@ -167,14 +167,14 @@
</MudPaper> </MudPaper>
<!-- Social Media --> <!-- Social Media -->
<MudPaper Elevation="3" Class="pa-6"> <MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">شبکه‌های اجتماعی</MudText> <MudText Typo="Typo.h6" Class="mb-4">شبکه‌های اجتماعی</MudText>
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Inherit" Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Telegram" StartIcon="@Icons.Custom.Brands.Telegram"
Href="https://t.me/kbs1" Href="@(_socialTelegram ?? "https://t.me/kbs1")"
Target="_blank" Target="_blank"
FullWidth="true"> FullWidth="true">
تلگرام تلگرام
@@ -183,7 +183,7 @@
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Inherit" Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Instagram" StartIcon="@Icons.Custom.Brands.Instagram"
Href="https://instagram.com/kbs1" Href="@(_socialInstagram ?? "https://instagram.com/kbs1")"
Target="_blank" Target="_blank"
FullWidth="true"> FullWidth="true">
اینستاگرام اینستاگرام
@@ -192,7 +192,7 @@
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Inherit" Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.LinkedIn" StartIcon="@Icons.Custom.Brands.LinkedIn"
Href="https://linkedin.com/company/kbs1" Href="@(_socialLinkedin ?? "https://linkedin.com/company/kbs1")"
Target="_blank" Target="_blank"
FullWidth="true"> FullWidth="true">
لینکدین لینکدین
@@ -201,7 +201,7 @@
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Inherit" Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.WhatsApp" StartIcon="@Icons.Custom.Brands.WhatsApp"
Href="https://wa.me/989123456789" Href="@(_socialWhatsapp ?? "https://wa.me/989123456789")"
Target="_blank" Target="_blank"
FullWidth="true"> FullWidth="true">
واتس‌اپ واتس‌اپ
@@ -219,14 +219,14 @@
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" Class="mb-8">موقعیت مکانی</MudText> <MudText Typo="Typo.h4" Align="Align.Center" Class="mb-8">موقعیت مکانی</MudText>
<MudPaper Elevation="3" Class="pa-4 overflow-hidden"> <MudPaper Elevation="2" Class="pa-4 overflow-hidden">
<!-- Placeholder for Map --> <!-- Placeholder for Map -->
<div style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center;"> <div class="contact-map-placeholder" style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; border-radius: 12px;">
<MudStack AlignItems="AlignItems.Center" Spacing="3"> <MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Large" Style="color: white;" /> <MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Large" Style="color: white;" />
<MudText Typo="Typo.h6" Style="color: white;">نقشه موقعیت مکانی</MudText> <MudText Typo="Typo.h6" Style="color: white;">نقشه موقعیت مکانی</MudText>
<MudText Typo="Typo.body2" Style="color: rgba(255,255,255,0.8);" Align="Align.Center"> <MudText Typo="Typo.body2" Style="color: rgba(255,255,255,0.8);" Align="Align.Center">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶ @(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText> </MudText>
</MudStack> </MudStack>
</div> </div>
+75 -4
View File
@@ -1,8 +1,10 @@
using FluentValidation; using FluentValidation;
using FrontOffice.Main.Utilities;
using MudBlazor; using MudBlazor;
using Severity = MudBlazor.Severity; using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages; namespace FrontOffice.Main.Pages;
public partial class Contact public partial class Contact
{ {
private ContactForm _contactForm = new(); private ContactForm _contactForm = new();
@@ -10,6 +12,60 @@ public partial class Contact
private bool _isSubmitting; private bool _isSubmitting;
private readonly ContactFormValidator _contactFormValidator = new(); private readonly ContactFormValidator _contactFormValidator = new();
// Dynamic CMS data
private SitePageDto? _page;
private string? _contactAddress;
private string? _contactPhone;
private string? _contactEmail;
private string? _contactHours;
private string? _socialTelegram;
private string? _socialInstagram;
private string? _socialLinkedin;
private string? _socialWhatsapp;
protected override async Task OnInitializedAsync()
{
try
{
_page = await SitePageService.GetByKeyAsync("contact");
if (_page != null)
{
// Load contact info from CMS section
var contactInfoSection = _page.GetSection("contact-info");
if (contactInfoSection != null)
{
var contactData = contactInfoSection.GetExtraData<ContactInfoData>();
if (contactData != null)
{
_contactAddress = contactData.Address;
_contactPhone = contactData.Phone;
_contactEmail = contactData.Email;
_contactHours = contactData.Hours;
}
}
// Load social media links from CMS section
var socialSection = _page.GetSection("social-media");
if (socialSection != null)
{
var socialData = socialSection.GetExtraData<SocialMediaData>();
if (socialData != null)
{
_socialTelegram = socialData.Telegram;
_socialInstagram = socialData.Instagram;
_socialLinkedin = socialData.Linkedin;
_socialWhatsapp = socialData.Whatsapp;
}
}
}
}
catch
{
// Fallback: all values remain null → hardcoded defaults will render
}
}
private async Task SubmitContactForm() private async Task SubmitContactForm()
{ {
if (_form is null) return; if (_form is null) return;
@@ -55,14 +111,12 @@ public partial class Contact
private void CallSupport() private void CallSupport()
{ {
// TODO: Initiate phone call or show phone number Snackbar.Add($"شماره تماس: {_contactPhone ?? "026-34233563"}", Severity.Info);
Snackbar.Add("شماره تماس: ۰۲۱-۱۲۳۴۵۶۷۸", Severity.Info);
} }
private void SendEmail() private void SendEmail()
{ {
// TODO: Open email client or redirect to email page Snackbar.Add($"ایمیل: {_contactEmail ?? "info@kbs1.co"}", Severity.Info);
Snackbar.Add("ایمیل: info@kbs1.co", Severity.Info);
} }
public class ContactForm public class ContactForm
@@ -87,4 +141,21 @@ public partial class Contact
RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد"); RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد");
} }
} }
// DTO classes for ExtraData JSON deserialization
private class ContactInfoData
{
public string? Address { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Hours { get; set; }
}
private class SocialMediaData
{
public string? Telegram { get; set; }
public string? Instagram { get; set; }
public string? Linkedin { get; set; }
public string? Whatsapp { get; set; }
}
} }
@@ -0,0 +1,190 @@
@attribute [Route(RouteConstants.DiscountStore.Cart)]
<PageTitle>سبد خرید تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سبد خرید تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید تخفیفی شما خالی است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
بازگشت به فروشگاه تخفیفی
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="DiscountCart.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
تا @context.MaxDiscountPercent%
</MudChip>
</MudTd>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Remove" Color="Color.Default"
OnClick="@(() => DecrementQty(context.ProductId))" />
<MudText>@context.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Default"
OnClick="@(() => IncrementQty(context.ProductId))" />
</MudStack>
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(context.ProductId)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var item in DiscountCart.Items)
{
<MudPaper Class="pa-3 rounded-lg w-100-mobile" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
تا @item.MaxDiscountPercent%
</MudChip>
</MudStack>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Class="mud-text-secondary">@FormatPrice(item.UnitPrice) واحد</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton
Icon="@(item.Quantity <= 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Default"
OnClick="@(() => DecrementQty(item.ProductId))" />
<MudText>@item.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Default"
OnClick="@(() => IncrementQty(item.ProductId))" />
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
@if (item.DiscountAmount > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">تخفیف: @FormatPrice(item.DiscountAmount)-</MudText>
}
<MudText>جمع: @FormatPrice(item.FinalPrice)</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(item.ProductId)" />
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<!-- Summary -->
@if (DeviceDetector.IsMobile())
{
<MudStack Spacing="1" Class="mobile-actions-stack px-5">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
<MudText Typo="Typo.caption" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">حداقل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه خرید</MudButton>
</MudStack>
}
else
{
<MudPaper Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">حداقل مبلغ پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد.
</MudText>
</MudStack>
</MudPaper>
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه فرایند خرید</MudButton>
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Cart : IDisposable
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
}
private async Task IncrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity + 1);
}
private async Task DecrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
if (item.Quantity <= 1)
{
await DiscountCart.RemoveAsync(productId);
}
else
{
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity - 1);
}
}
private async Task Remove(long productId)
{
await DiscountCart.RemoveAsync(productId);
}
private void ProceedCheckout()
{
Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout);
}
private static string FormatPrice(long price) => $"{price:N0} ";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
@@ -0,0 +1,193 @@
@using CMSMicroservice.Protobuf.Protos.UserAddress
@attribute [Route(RouteConstants.DiscountStore.Checkout)]
<PageTitle>تسویه حساب فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تسویه حساب تخفیفی" BackHref="@RouteConstants.DiscountStore.Cart" />
<MudGrid Spacing="3">
<!-- Left Column: Address + Payment Settings -->
<MudItem xs="12" md="8">
<!-- Address Selection -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudText Typo="Typo.h6" Class="mb-2">انتخاب آدرس</MudText>
@if (_loadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
<MudStack Spacing="1">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack>
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
</MudStack>
}
</MudPaper>
<!-- Discount Balance (Hybrid Payment) -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Color="Color.Secondary" />
<MudText Typo="Typo.h6">پرداخت از کیف تخفیفی</MudText>
</MudStack>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudText Typo="Typo.body2">موجودی کیف تخفیفی شما:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Secondary" Class="fw-bold">
@FormatPrice(_discountBalance) تومان
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudText Typo="Typo.body2">سقف مجاز تخفیف روی این سفارش:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Success">
@FormatPrice(MaxAllowedDiscount) تومان
</MudText>
</MudStack>
<MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
مبلغی که از کیف تخفیفی کسر شود (باقیمانده از درگاه پرداخت می‌شود):
</MudText>
<MudSlider T="long" @bind-Value="_discountBalanceToUse"
Min="0" Max="@MaxUsable"
Step="1000"
Color="Color.Secondary"
Immediate="true">
استفاده از @FormatPrice(_discountBalanceToUse) تومان
</MudSlider>
<MudNumericField T="long" @bind-Value="_discountBalanceToUse"
Min="0" Max="@MaxUsable"
Label="مبلغ دلخواه (تومان)" Variant="Variant.Outlined"
Margin="Margin.Dense" Immediate="true" />
@if (_discountBalanceToUse > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
مبلغ @FormatPrice(_discountBalanceToUse) تومان از کیف تخفیفی کسر و
مبلغ @FormatPrice(GatewayAmount) تومان از درگاه پرداخت دریافت خواهد شد.
</MudAlert>
}
</MudStack>
</MudPaper>
<!-- Notes -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">توضیحات سفارش</MudText>
<MudTextField T="string" @bind-Value="_notes" Lines="3"
Placeholder="توضیحات اختیاری..."
Variant="Variant.Outlined" />
</MudPaper>
</MudItem>
<!-- Right Column: Order Summary -->
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="position:sticky; top:80px;">
<MudText Typo="Typo.h6" Class="mb-2">خلاصه سفارش</MudText>
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
}
else
{
<MudList T="string" Dense="true">
@foreach (var item in DiscountCart.Items)
{
<MudListItem T="string">
<MudListItemText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="40" ImgHeight="40" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@item.Quantity × @FormatPrice(item.UnitPrice)
</MudText>
@if (item.MaxDiscountPercent > 0)
{
<MudText Typo="Typo.caption" Color="Color.Error">تا @item.MaxDiscountPercent% تخفیف</MudText>
}
</MudStack>
</MudStack>
</MudListItemText>
</MudListItem>
}
</MudList>
<MudDivider Class="my-2" />
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice)</MudText>
</MudStack>
@if (_discountBalanceToUse > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">از کیف تخفیفی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_discountBalanceToUse)-</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">پرداخت از درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(GatewayAmount) تومان
</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مالیات بر ارزش افزوده صرفاً بر مبلغ درگاه محاسبه خواهد شد.
</MudText>
</MudStack>
<MudButton Disabled="@(!CanPlaceOrder || _placing)" Class="mt-3 w-100-mobile"
Variant="Variant.Filled" Color="Color.Primary"
OnClick="PlaceOrder" FullWidth="true"
StartIcon="@Icons.Material.Filled.CheckCircle">
@(_placing ? "در حال ثبت سفارش..." : "ثبت و پرداخت سفارش")
</MudButton>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
@@ -0,0 +1,146 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using MudBlazor;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Checkout
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private long _discountBalance;
private long _discountBalanceToUse;
private string? _notes;
private bool _placing;
/// <summary>
/// Maximum discount allowed based on cart items' MaxDiscountPercent
/// </summary>
private long MaxAllowedDiscount => DiscountCart.TotalDiscount;
/// <summary>
/// Maximum the user can actually use = min(balance, allowed discount)
/// </summary>
private long MaxUsable => Math.Min(_discountBalance, MaxAllowedDiscount);
/// <summary>
/// Amount to be charged via payment gateway
/// </summary>
private long GatewayAmount => DiscountCart.TotalPrice - _discountBalanceToUse;
private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
var addressTask = LoadAddresses();
var balanceTask = LoadDiscountBalance();
await Task.WhenAll(addressTask, balanceTask);
// Default: use maximum possible discount
_discountBalanceToUse = MaxUsable;
}
private async Task LoadDiscountBalance()
{
try
{
var balances = await WalletService.GetBalancesAsync();
_discountBalance = balances.DiscountBalance;
}
catch
{
_discountBalance = 0;
}
}
private async Task LoadAddresses()
{
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
}
finally
{
_loadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
{
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
_placing = true;
try
{
// Clamp the discount balance to use
var discountToUse = Math.Min(_discountBalanceToUse, MaxUsable);
var result = await DiscountOrderService.PlaceOrderAsync(
_selectedAddress.Id,
discountToUse,
_notes);
if (!result.Success)
{
Snackbar.Add(result.Message, Severity.Error);
return;
}
// If there's a gateway payment URL, redirect to it
if (!string.IsNullOrWhiteSpace(result.PaymentUrl) && result.GatewayAmount > 0)
{
Snackbar.Add("در حال انتقال به درگاه پرداخت...", Severity.Info);
await DiscountCart.ClearAsync();
Navigation.NavigateTo(result.PaymentUrl, forceLoad: true);
return;
}
// If fully paid via discount balance (no gateway needed)
await DiscountCart.ClearAsync();
Snackbar.Add("سفارش با موفقیت ثبت شد!", Severity.Success);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{result.OrderId}");
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
}
finally
{
_placing = false;
}
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -0,0 +1,198 @@
@attribute [Route("/discount-store/order/{Id:long}")]
<PageTitle>جزئیات سفارش تخفیفی</PageTitle>
@if (!string.IsNullOrEmpty(_paymentMessage))
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="pt-4">
<MudAlert Severity="@_paymentSeverity" Variant="Variant.Filled" Class="mb-2"
CloseIconClicked="() => _paymentMessage = null" ShowCloseIcon="true">
@_paymentMessage
</MudAlert>
</MudContainer>
}
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<LoadingState />
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش مورد نظر یافت نشد."
ActionText="بازگشت به لیست سفارش‌ها"
ActionHref="@RouteConstants.DiscountStore.Orders" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Header -->
<PageHeader Title="@($"سفارش #{_order.OrderNumber}")" BackHref="@RouteConstants.DiscountStore.Orders" />
<MudStack Row="true" Spacing="2" Class="flex-wrap mb-4">
<MudChip T="string" Color="@(_order.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(_order.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت")
</MudChip>
<MudChip T="string" Color="@DiscountOrderService.GetDeliveryStatusColor(_order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(_order.DeliveryStatus)
</MudChip>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
تاریخ ثبت: @FormatDate(_order.Created)
</MudText>
@* ── Address ── *@
@if (_order.Address is not null)
{
<MudText Typo="Typo.body2">
<b>آدرس:</b> @_order.Address.Title — @_order.Address.Address
</MudText>
@if (!string.IsNullOrWhiteSpace(_order.Address.PostalCode))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @_order.Address.PostalCode</MudText>
}
}
<MudDivider Class="my-2" />
@* ── Desktop: Table ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudTable Items="_order.Items" Dense="true">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Title</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">@context.MaxDiscountPercent%</MudChip>
}
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@* ── Mobile: Cards ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudStack Spacing="2">
@foreach (var item in _order.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت واحد:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(item.UnitPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">تعداد:</MudText>
<MudText Typo="Typo.body2">@item.Count</MudText>
</MudStack>
@if (item.DiscountAmount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">تخفیف (@item.MaxDiscountPercent%):</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(item.DiscountAmount)-</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">قیمت نهایی:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudDivider Class="my-2" />
@* ── Financial Summary ── *@
<MudStack Spacing="1" Class="pa-3" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudText Typo="Typo.h6" Class="mb-1">خلاصه مالی</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_order.TotalPrice)</MudText>
</MudStack>
@if (_order.DiscountBalanceUsed > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">از کیف تخفیفی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_order.DiscountBalanceUsed)-</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(_order.GatewayAmount)
</MudText>
</MudStack>
</MudStack>
@* ── Tracking / Transaction ── *@
@if (!string.IsNullOrWhiteSpace(_order.TrackingCode))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">کد رهگیری:</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@_order.TrackingCode</MudText>
</MudStack>
}
@if (!string.IsNullOrWhiteSpace(_order.TransactionId))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه تراکنش:</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_order.TransactionId</MudText>
</MudStack>
}
@* ── Notes ── *@
@if (!string.IsNullOrWhiteSpace(_order.Notes))
{
<MudDivider Class="my-1" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
<b>توضیحات:</b> @_order.Notes
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_order.AdminNotes))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
<b>پیام مدیر:</b> @_order.AdminNotes
</MudAlert>
}
</MudStack>
</MudPaper>
</MudContainer>
}
@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class OrderDetail
{
[Parameter] public long Id { get; set; }
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentResult { get; set; }
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private DiscountOrderDetail? _order;
private bool _loading = true;
private string? _paymentMessage;
private MudBlazor.Severity _paymentSeverity;
protected override async Task OnInitializedAsync()
{
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
if (!string.IsNullOrEmpty(PaymentResult))
{
switch (PaymentResult.ToLower())
{
case "success":
_paymentMessage = "پرداخت با موفقیت انجام شد! سفارش شما ثبت نهایی شد.";
_paymentSeverity = MudBlazor.Severity.Success;
break;
case "failed":
_paymentMessage = "پرداخت ناموفق بود. مبلغ رزرو شده آزاد خواهد شد.";
_paymentSeverity = MudBlazor.Severity.Error;
break;
case "error":
_paymentMessage = "خطایی در پردازش پرداخت رخ داد. لطفاً با پشتیبانی تماس بگیرید.";
_paymentSeverity = MudBlazor.Severity.Warning;
break;
}
}
await LoadOrderAsync();
}
private async Task LoadOrderAsync()
{
_loading = true;
try
{
_order = await DiscountOrderService.GetOrderByIdAsync(Id);
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,130 @@
@attribute [Route(RouteConstants.DiscountStore.Orders)]
<PageTitle>سفارش‌های فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سفارش‌های تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
@if (_loading)
{
<LoadingState />
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
Href="@RouteConstants.DiscountStore.Products">
مشاهده فروشگاه تخفیفی
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="_orders">
<HeaderContent>
<MudTh>شماره سفارش</MudTh>
<MudTh>تعداد اقلام</MudTh>
<MudTh>مبلغ کل</MudTh>
<MudTh>از کیف تخفیفی</MudTh>
<MudTh>درگاه</MudTh>
<MudTh>وضعیت پرداخت</MudTh>
<MudTh>وضعیت ارسال</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.OrderNumber</MudTd>
<MudTd>@context.ItemsCount</MudTd>
<MudTd>@FormatPrice(context.TotalPrice)</MudTd>
<MudTd>
@if (context.DiscountBalanceUsed > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountBalanceUsed)</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.GatewayAmount)</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@(context.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(context.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت")
</MudChip>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(context.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(context.DeliveryStatus)
</MudChip>
</MudTd>
<MudTd>@FormatDate(context.Created)</MudTd>
<MudTd>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => ViewOrder(context.Id)">
جزئیات
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var order in _orders)
{
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
@onclick="() => ViewOrder(order.Id)">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">سفارش #@order.OrderNumber</MudText>
<MudChip T="string" Size="Size.Small"
Color="@(order.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(order.PaymentCompleted ? "پرداخت شده" : "در انتظار")
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order.Created)</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(order.DeliveryStatus)
</MudChip>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">@order.ItemsCount قلم</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(order.TotalPrice) تومان</MudText>
</MudStack>
@if (order.DiscountBalanceUsed > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">
از کیف تخفیفی: @FormatPrice(order.DiscountBalanceUsed) تومان
</MudText>
}
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="@_totalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged" Color="Color.Primary"
Variant="Variant.Outlined" />
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Orders
{
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private List<DiscountOrderSummary> _orders = new();
private int _currentPage = 1;
private int _totalPages;
private bool _loading = true;
private const int PageSize = 10;
protected override async Task OnInitializedAsync()
{
await LoadOrdersAsync();
}
private async Task LoadOrdersAsync()
{
_loading = true;
try
{
var result = await DiscountOrderService.GetUserOrdersAsync(_currentPage, PageSize);
_orders = result.Orders;
_totalPages = result.TotalPages;
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش‌ها", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadOrdersAsync();
}
private void ViewOrder(long orderId)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{orderId}");
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,140 @@
@attribute [Route("/discount-store/product/{Id:long}")]
<PageTitle>@(_product?.Title ?? "محصول تخفیفی")</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4 py-md-6">
@if (_loading)
{
<LoadingState />
}
else if (_product is null)
{
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="محصول مورد نظر یافت نشد."
ActionText="بازگشت به فروشگاه تخفیفی"
ActionHref="@RouteConstants.DiscountStore.Products" />
}
else
{
<!-- Breadcrumb -->
<PageHeader Title="جزئیات محصول تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
<MudGrid Spacing="4">
<!-- Image Gallery -->
<MudItem xs="12" md="5">
<MudPaper Class="pa-2 rounded-xl" Elevation="1">
<AppImage Path="@_selectedImage" Alt="@_product.Title"
ObjectFit="ObjectFit.Contain"
Style="width:100%; max-height:400px; border-radius:12px;" />
@if (_product.Images.Count > 1)
{
<MudStack Row="true" Spacing="1" Class="mt-2 flex-wrap" Justify="Justify.Center">
@foreach (var img in _product.Images)
{
<span @onclick="() => _selectedImage = img.ImageUrl" style="cursor:pointer">
<AppImage Path="@img.ThumbnailUrl" Alt="@(img.Title ?? "")"
ImgWidth="60" ImgHeight="60"
ObjectFit="ObjectFit.Cover"
Class="rounded-lg"
Style="@($"border: 2px solid {(img.ImageUrl == _selectedImage ? "var(--mud-palette-primary)" : "transparent")};")"
/>
</span>
}
</MudStack>
}
</MudPaper>
</MudItem>
<!-- Product Info -->
<MudItem xs="12" md="7">
<MudStack Spacing="3">
<MudText Typo="Typo.h5" Class="fw-bold">@_product.Title</MudText>
<!-- Categories -->
@if (_product.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var cat in _product.Categories)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">@cat.Title</MudChip>
}
</MudStack>
}
<!-- Price & Discount -->
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
@if (_product.MaxDiscountPercent > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف پرداخت از کیف تخفیفی:</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
تا @_product.MaxDiscountPercent% (تا @($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
</MudChip>
</MudStack>
}
</MudStack>
</MudPaper>
<!-- Stock & Stats -->
<MudStack Row="true" Spacing="3" Class="flex-wrap">
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small"
Color="@(_product.RemainingCount > 0 ? Color.Success : Color.Error)"
Icon="@Icons.Material.Filled.Inventory">
@(_product.RemainingCount > 0 ? $"موجود ({_product.RemainingCount} عدد)" : "ناموجود")
</MudChip>
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.Visibility">
@_product.ViewCount بازدید
</MudChip>
</MudStack>
<!-- Short Info -->
@if (!string.IsNullOrWhiteSpace(_product.ShortInfo))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.ShortInfo</MudText>
}
<!-- Add to Cart -->
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudNumericField T="int" @bind-Value="_quantity" Min="1"
Max="@(_product.RemainingCount > 0 ? _product.RemainingCount : 1)"
Label="تعداد" Variant="Variant.Outlined" Margin="Margin.Dense"
Style="width:100px;" />
<MudButton Variant="Variant.Filled" Color="Color.Success"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
OnClick="AddToCart"
Disabled="@(_product.RemainingCount <= 0 || _addingToCart)"
Size="Size.Large">
@(_addingToCart ? "در حال افزودن..." : "افزودن به سبد")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
مشاهده سبد
</MudButton>
</MudStack>
</MudStack>
</MudItem>
</MudGrid>
<!-- Full Information -->
@if (!string.IsNullOrWhiteSpace(_product.FullInfo))
{
<MudPaper Elevation="1" Class="pa-4 pa-md-6 mt-6 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">توضیحات محصول</MudText>
<MudDivider Class="mb-4" />
<div class="blog-content">
@((MarkupString)_product.FullInfo)
</div>
</MudPaper>
}
}
</MudContainer>
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class ProductDetail
{
[Parameter] public long Id { get; set; }
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private DiscountProductDetail? _product;
private string _selectedImage = string.Empty;
private int _quantity = 1;
private bool _loading = true;
private bool _addingToCart;
protected override async Task OnInitializedAsync()
{
await LoadProductAsync();
}
private async Task LoadProductAsync()
{
_loading = true;
try
{
_product = await DiscountProductService.GetByIdAsync(Id);
if (_product is not null)
{
_selectedImage = _product.Images.FirstOrDefault()?.ImageUrl
?? _product.ThumbnailUrl;
}
}
catch
{
Snackbar.Add("خطا در بارگذاری محصول", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task AddToCart()
{
if (_product is null) return;
_addingToCart = true;
try
{
await DiscountCartService.AddAsync(_product.Id, _quantity);
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
}
catch
{
Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error);
}
finally
{
_addingToCart = false;
}
}
}
@@ -0,0 +1,133 @@
@attribute [Route(RouteConstants.DiscountStore.Products)]
<PageTitle>فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Hero -->
<MudPaper Class="discount-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="dash-hero-name" />
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="dash-hero-sub">
خرید با استفاده از موجودی کیف پول تخفیفی
</MudText>
</MudStack>
</MudPaper>
<!-- Filters -->
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="5">
<MudTextField @bind-Value="_search"
Placeholder="جستجو در محصولات..."
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Immediate="true"
OnKeyUp="OnSearchKeyUp"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="8" sm="5">
<MudSelect T="long?" Value="_selectedCategoryId" ValueChanged="OnCategoryChanged"
Label="دسته‌بندی" Variant="Variant.Outlined" Margin="Margin.Dense"
Clearable="true" AnchorOrigin="Origin.BottomCenter">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title (@cat.ProductCount)</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4" sm="2" Class="d-flex gap-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.Search" OnClick="SearchProducts"
Style="height:40px;">جستجو</MudButton>
</MudItem>
<MudItem xs="12" Class="d-flex justify-start justify-md-end gap-2">
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
سبد خرید
@if (DiscountCart.Count > 0)
{
<MudBadge Content="@DiscountCart.Count" Color="Color.Error" Overlap="true" Class="ms-2" />
}
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
<!-- Loading -->
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_result.Products.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
}
else
{
<!-- Products Grid -->
<MudGrid Spacing="1">
@foreach (var p in _result.Products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;height: 300px">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="height: 60%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
@if (p.MaxDiscountPercent > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;left:8px;">
تا @p.MaxDiscountPercent% تخفیف
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
</div>
</MudCardContent>
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => AddToCart(p))"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<!-- Pagination -->
@if (_result.TotalPages > 1)
{
<div class="d-flex justify-center mt-4">
<MudPagination Count="@_result.TotalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged"
Color="Color.Primary" Variant="Variant.Filled"
BoundaryCount="1" MiddleCount="3" />
</div>
}
}
</MudContainer>
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private DiscountProductService ProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
private string _search = string.Empty;
private long? _selectedCategoryId;
private int _currentPage = 1;
private bool _loading;
private const int PageSize = 12;
private DiscountProductListResult _result = new(new(), 0, 0, 1);
private List<DiscountCategoryNode> _categories = new();
protected override async Task OnInitializedAsync()
{
_loading = true;
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
var categoriesTask = ProductService.GetCategoriesAsync();
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
await Task.WhenAll(categoriesTask, productsTask);
_categories = categoriesTask.Result;
_result = productsTask.Result;
_loading = false;
}
private async Task LoadProductsAsync()
{
_loading = true;
StateHasChanged();
_result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId);
_loading = false;
StateHasChanged();
}
private async Task SearchProducts() => await LoadProductsAsync();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
_currentPage = 1;
await LoadProductsAsync();
}
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
_currentPage = 1;
await LoadProductsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadProductsAsync();
}
private async Task AddToCart(DiscountProductCard p)
{
await DiscountCart.AddAsync(p.Id);
}
private void NavigateToProduct(long id)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
+1 -1
View File
@@ -116,7 +116,7 @@
<!-- Contact Support --> <!-- Contact Support -->
<section class="py-12 bg-grey-50"> <section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudPaper Elevation="3" Class="pa-8 text-center"> <MudPaper Elevation="2" Class="pa-8 text-center">
<MudIcon Icon="@Icons.Material.Filled.ContactSupport" Size="Size.Large" Color="Color.Primary" Class="mb-4" /> <MudIcon Icon="@Icons.Material.Filled.ContactSupport" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h4" Class="mb-3">سوال شما پاسخ داده نشد؟</MudText> <MudText Typo="Typo.h4" Class="mb-3">سوال شما پاسخ داده نشد؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6"> <MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
@@ -0,0 +1,62 @@
@attribute [Route(RouteConstants.Gateway.CartChooser)]
<PageTitle>سبد خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سبد خرید" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سبد خرید کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید فروشگاه</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_cartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید تخفیفی</MudText>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_discountCartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class CartChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,49 @@
@attribute [Route(RouteConstants.Gateway.OrdersChooser)]
@inject IJSRuntime JS
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.Receipt" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سفارشات کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید معمولی</MudText>
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید تخفیفی</MudText>
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@code {
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,56 @@
@attribute [Route(RouteConstants.Gateway.StoreChooser)]
<PageTitle>فروشگاه‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="انتخاب فروشگاه" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary" Style="max-width:500px;">
برای خرید، یکی از فروشگاه‌ها را انتخاب کنید. هر فروشگاه سبد خرید و سفارشات مجزای خود را دارد.
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<!-- Regular Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
خرید محصولات با پرداخت از کیف پول اعتباری
</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_cartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
<!-- Discount Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
خرید با استفاده از موجودی کیف تخفیفی + درگاه پرداخت
</MudText>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_discountCartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
</MudStack>
</MudContainer>
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class StoreChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
}
+318 -329
View File
@@ -1,354 +1,343 @@
@attribute [Route(RouteConstants.Main.MainPage)] @attribute [Route(RouteConstants.Main.MainPage)]
@inject NavigationManager Navigation @inject IJSRuntime JS
<PageTitle>صفحه اصلی</PageTitle> <PageTitle>صفحه اصلی | کارا بازار سلامت</PageTitle>
<!-- HERO --> @* ═══════════════════════════════════════════════
<section id="hero" class="hero-section"> 1. HERO — Bold, minimal, centered
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16"> ═══════════════════════════════════════════════ *@
<MudGrid Justify="Justify.Center" Spacing="3"> <section class="hero-gradient py-12 py-md-16">
<MudItem xs="12" md="6"> <MudContainer MaxWidth="MaxWidth.Medium">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-3">پیشنهاد ویژه برای شروع</MudChip> <MudStack AlignItems="AlignItems.Center" Spacing="4" Class="text-center">
<MudStack Spacing="2"> <MudChip T="string" Color="Color.Default" Variant="Variant.Filled"
<MudText Typo="Typo.h1"> Class="pulse-chip hero-chip-glass" Size="Size.Small">
با دعوت از دوستان خود، از خریدهای واقعی پاداش بگیرید 🌿 پلتفرم سلامت‌محور فروش و تیم‌سازی
</MudText> </MudChip>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
ثبت‌نام آسان، تجربهٔ سریع و شفاف. همین امروز حساب بساز و با دعوت دوستانت، امتیاز و پاداش دریافت کن.
</MudText>
<MudStack Row="true" Spacing="3" Class="mb-2"> <MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
<MudButton Variant="Variant.Outlined" رشد تیم، فروش واقعی، پاداش شفاف
Color="Color.Primary" </MudText>
Size="Size.Large"
Class="mud-ripple rounded-pill" <MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
OnClick="@(() => Navigation.NavigateTo("/pricing"))">مشاهده قیمت‌ها</MudButton> با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.
<MudButton Variant="Variant.Filled" </MudText>
Color="Color.Primary"
Size="Size.Large" <MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
Class="mud-ripple rounded-pill" <MudButton Variant="Variant.Filled"
OnClick="@(() => NavigateToRegistrationWizard())">شروع کن</MudButton> Class="rounded-pill hero-cta-primary"
</MudStack> Size="Size.Large"
<MudDivider Class="my-2" Style="width: 90%;" /> OnClick="NavigateToRegistrationWizard">
<MudStack Row="true" Spacing="1"> شروع رایگان
<MudChip T="string" </MudButton>
Color="Color.Info" <MudButton Variant="Variant.Outlined"
Variant="Variant.Outlined" Class="rounded-pill hero-cta-outline"
Class="mb-3">ثبت‌نام زیر ۲ دقیقه</MudChip> Size="Size.Large"
<MudChip T="string" OnClick="@(() => Navigation.NavigateTo("/blog"))">
Color="Color.Info" آخرین اخبار
Variant="Variant.Outlined" </MudButton>
Class="mb-3">پشتیبانی ۷×۲۴</MudChip> </MudStack>
<MudChip T="string"
Color="Color.Info" @* ── Trust badges ── *@
Variant="Variant.Outlined" <MudStack Row="true" Spacing="4" Class="mt-4 flex-wrap" Justify="Justify.Center" AlignItems="AlignItems.Center">
Class="mb-3">پرداخت ایمن</MudChip> @foreach (var badge in _trustBadges)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@badge.Icon" Size="Size.Small" Style="color:rgba(255,255,255,.7);" />
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,.8);">@badge.Text</MudText>
</MudStack> </MudStack>
}
</MudStack>
</MudStack>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
1b. FEATURED POST BANNER — below hero
═══════════════════════════════════════════════ *@
@if (_latestPosts.Any())
{
var featured = _latestPosts.First();
<section class="py-4" style="background:var(--mud-palette-surface);">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="1" Class="landing-blog-banner rounded-xl cursor-pointer pa-3"
@onclick="() => NavigateToPost(featured.Slug)">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(featured.ThumbnailUrl))
{
<AppImage Path="@featured.ThumbnailUrl"
Alt="@featured.Title"
ObjectFit="ObjectFit.Cover"
Class="landing-blog-thumb"
Fallback="@Icons.Material.Filled.Article" />
}
else
{
<MudAvatar Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" Class="rounded-lg" Style="min-width:64px;">
<MudIcon Icon="@Icons.Material.Filled.Article" />
</MudAvatar>
}
<MudStack Spacing="1" Style="flex:1; min-width:0;">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary">
🔥 جدیدترین مطلب
</MudChip>
<MudText Typo="Typo.subtitle1" Class="landing-blog-title">@featured.Title</MudText>
@if (!string.IsNullOrWhiteSpace(featured.Summary))
{
<MudText Typo="Typo.body2" Class="landing-blog-summary mud-text-secondary">@featured.Summary</MudText>
}
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary d-none d-sm-flex" />
</MudStack> </MudStack>
</MudItem> </MudPaper>
</MudContainer>
</section>
}
<MudItem xs="12" md="6"> @* ═══════════════════════════════════════════════
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);"> 2. HOW IT WORKS — 3 numbered steps
<MudImage Src="images/team-meeting.jpg" ═══════════════════════════════════════════════ *@
Alt="جلسه تیم فروش" <section class="section-landing">
ObjectFit="ObjectFit.Cover" <MudContainer MaxWidth="MaxWidth.Large">
ObjectPosition="ObjectPosition.Center" <div class="text-center mb-8 fade-in-up">
Style="width:100%" <MudText Typo="Typo.h3">سه گام تا شروع</MudText>
Class="rounded-xl" /> <MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
</MudPaper> بدون پیچیدگی، سریع و آسان
</MudItem> </MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var (step, i) in _steps.Select((s, i) => (s, i)))
{
<MudItem xs="12" sm="4">
<MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 text-center fade-in-up" data-delay="@(i * 150)">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="mx-auto mb-3 landing-step-num">
<MudText Typo="Typo.h5" Style="color:#fff;font-weight:800;">@(i + 1)</MudText>
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@step.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@step.Desc</MudText>
</MudPaper>
</MudItem>
}
</MudGrid> </MudGrid>
</MudContainer> </MudContainer>
</section> </section>
<MudStack Spacing="4">
<!-- WHYUS --> @* ═══════════════════════════════════════════════
<section class="whyus-section"> 3. FEATURES — 6 cards with icons
<MudContainer MaxWidth="MaxWidth.Large" Class="whyus-section"> ═══════════════════════════════════════════════ *@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudText Typo="Typo.h4" GutterBottom="true"> <MudContainer MaxWidth="MaxWidth.Large">
چرا کاربران ما را انتخاب می‌کنند؟ <div class="text-center mb-8 fade-in-up">
</MudText> <MudText Typo="Typo.h3">چرا کارا بازار سلامت؟</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8"> <MudText Typo="Typo.body1" Class="mud-text-secondary mt-2" Style="max-width:540px;margin:0 auto;">
سه دلیل روشن برای شروع امروز. ابزارهایی ساده و قدرتمند برای رشد کسب‌وکار شما
</MudText> </MudText>
</div>
<MudGrid Spacing="3" Justify="Justify.Center"> <MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var (icon, title, desc, delay) in _features)
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.VerifiedUser"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">ثبت‌نام سریع و ساده</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
در چند گام کوتاه، حسابت را بساز و شروع کن.
</MudText>
</MudStack>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.StarBorder"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">مزایا و پاداش‌های شفاف</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.
</MudText>
</MudStack>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.Devices"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">اپلیکیشن واکنش‌گرا</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
تجربه‌ای روان در موبایل و دسکتاپ.
</MudText>
</MudStack>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</section>
@* <!-- FEATURES: معرفی پکیج‌ها -->
<section id="features" class="py-20">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" GutterBottom="true">
پکیج‌های سرمایه گذاری
</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
بر اساس اندازهٔ تیم خود انتخاب کنید.
</MudText>
@if (_isLoadingPackages)
{ {
<MudStack AlignItems="AlignItems.Center" Class="py-8"> <MudItem xs="12" sm="6" md="4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" /> <MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 fade-in-up" data-delay="@delay">
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری پکیج‌ها...</MudText> <MudStack Spacing="2">
</MudStack> <MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Medium" Class="mb-1">
<MudIcon Icon="@icon" />
</MudAvatar>
<MudText Typo="Typo.h6">@title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@desc</MudText>
</MudStack>
</MudPaper>
</MudItem>
} }
else if (_packs.Any()) </MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
4. STATS — Counter cards
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var stat in _stats)
{ {
<MudGrid Spacing="4" Justify="Justify.Center" Class="stretch-grid"> <MudItem xs="6" sm="3">
@foreach (var p in _packs) <MudPaper Elevation="2" Class="pa-4 rounded-xl stat-card fade-in-up text-center">
{ <MudText Class="stat-number" Color="@stat.Color" id="@stat.ElementId">@stat.Display</MudText>
<MudItem xs="12" md="4" Class="d-flex"> <MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">@stat.Label</MudText>
<MudCard> </MudPaper>
<MudCardMedia Image="@p.Image" </MudItem>
Title="@p.Title" }
Height="300" /> </MudGrid>
<MudCardContent> </MudContainer>
<MudText Typo="Typo.h5" Class="mb-3">@(p.Title)</MudText> </section>
@((MarkupString)p.Body)
</MudCardContent> @* ═══════════════════════════════════════════════
<MudCardActions Class="ma-2"> 5. LATEST BLOG POSTS
<MudText Typo="Typo.h5" ═══════════════════════════════════════════════ *@
Color="Color.Success" @if (_latestPosts.Any())
Align="Align.End">@(p.Price.ToThousands().ToCurrencyUnitIRT())</MudText> {
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">آخرین مقالات</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
جدیدترین مطالب و اخبار کارا بازار سلامت
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var post in _latestPosts.Take(2))
{
<MudItem xs="12" sm="6" md="6">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer fade-in-up" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
<div class="blog-thumb" style="height:180px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:180px;" />
}
else
{
<div style="height:180px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.5);" />
</div>
}
</div>
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Summary
</MudText>
}
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mt-2">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer /> <MudSpacer />
<MudButton Variant="Variant.Filled" <MudText Typo="Typo.caption" Class="mud-text-secondary">
Color="Color.Primary" <MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
OnClick="@(() => NavigateToPackage(p.Id))">مشاهده جزئیات</MudButton> @post.ViewCount
</MudCardActions> </MudText>
</MudCard> </MudStack>
</MudItem> </div>
}
</MudGrid>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudText Typo="Typo.body1" Class="mud-text-secondary">هیچ پکیجی یافت نشد.</MudText>
</MudStack>
}
</MudContainer>
</section>
*@
<!-- STATS -->
<section class="stats-strip">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
کارا بازار سلامت چطور کار می‌کند؟
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
سه گام روشن تا شروع یک ماجراجویی جدید
</MudText>
<MudGrid>
<MudItem xs="12" lg="6">
<MudStack AlignItems="AlignItems.Start">
<MudPaper Outlined="true" Class="pa-6 rounded-xl">
<MudTimeline>
<MudTimelineItem Color="Color.Info" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Info" Typo="Typo.h5">ثبت‌نام</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Info" Typo="Typo.h6" GutterBottom="true">مرحله اول</MudText>
<MudText>یک حساب بساز و وارد شو.</MudText>
</ItemContent>
</MudTimelineItem>
<MudTimelineItem Color="Color.Success" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Success" Typo="Typo.h5">دعوت دوستان</MudText>
</ItemOpposite>
<ItemContent>
<MudText Align="Align.End" Color="Color.Success" Typo="Typo.h6" GutterBottom="true">مرحله دوم</MudText>
<MudText Align="Align.End">لینک دعوتت را به اشتراک بگذار.</MudText>
</ItemContent>
</MudTimelineItem>
<MudTimelineItem Color="Color.Error" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Error" Typo="Typo.h5">دریافت پاداش</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Error" Typo="Typo.h6" GutterBottom="true">مرحله سوم</MudText>
<MudText>از خریدهای واقعی دوستانت پاداش بگیر.</MudText>
</ItemContent>
</MudTimelineItem>
</MudTimeline>
</MudPaper> </MudPaper>
</MudStack> </MudItem>
</MudItem> }
<MudItem xs="1">
<MudDivider Vertical="true" />
</MudItem>
<MudItem xs="12" lg="5" Class="d-flex align-center">
<MudStack>
<MudGrid Spacing="4" Justify="Justify.SpaceAround">
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl ">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Success">+۵۰٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">رشد میانگین تیم</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Primary">۹۹٫۹٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">آپ‌تایم سرویس</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Warning">۳ روز</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">میانگین زمان استقرار</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Secondary">+۲۰ کشور</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">پوشش ارسال کد</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
</MudItem>
</MudGrid> </MudGrid>
</MudContainer> <div class="text-center mt-6">
</section> <MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="/blog">
<!-- TESTIMONIALS --> مشاهده همه مقالات
<section id="testimonials" class="py-20 testimonials"> </MudButton>
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
اعتماد مشتریان
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
بخشی از تجربهٔ استفاده از «کارا بازار سلامت».
</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar1.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">شرکت سینا نت</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر عملیات</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«با کارا بازار سلامت، محاسبهٔ کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar2.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">هولدینگ آریانا</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر فروش</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«شجره‌نامهٔ بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- FAQ -->
<section id="faq" class="mb-6">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
سوالات متداول
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
پاسخ به سوالات رایج شما.
</MudText>
<div class="mx-auto max-w-800">
<MudExpansionPanels Square="false" Class=" ">
@foreach (var q in _faqs)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2">@q.A</MudText>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div> </div>
</MudContainer> </MudContainer>
</section> </section>
}
</MudStack> @* ═══════════════════════════════════════════════
6. TESTIMONIALS
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">اعتماد مشتریان</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بخشی از تجربه استفاده از «کارا بازار سلامت»
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var t in _testimonials)
{
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-5 rounded-xl testimonial-card fade-in-up" data-delay="@t.Delay">
<MudStack Spacing="2">
<MudRating ReadOnly="true" SelectedValue="5" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
«@t.Quote»
</MudText>
<MudDivider Class="my-1" />
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">@t.Name</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">@t.Role</MudText>
</div>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
7. FAQ
═══════════════════════════════════════════════ *@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Medium">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">سوالات متداول</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
پاسخ به سوالات رایج شما
</MudText>
</div>
<div class="fade-in-up">
<MudExpansionPanels Elevation="1" Class="rounded-xl">
@foreach (var q in _faqs)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@q.A</MudText>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
8. CTA BANNER
═══════════════════════════════════════════════ *@
<section class="section-landing-lg">
<MudContainer MaxWidth="MaxWidth.Medium">
<div class="cta-banner pa-8 pa-md-12 fade-in-up">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h3" Align="Align.Center" Class="dash-hero-name">
آماده شروع هستید؟
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Size="Size.Large"
Class="rounded-pill mt-2 hero-cta-primary"
OnClick="NavigateToRegistrationWizard">
شروع رایگان
</MudButton>
</MudStack>
</div>
</MudContainer>
</section>
+107 -81
View File
@@ -1,44 +1,66 @@
using CMSMicroservice.Protobuf.Protos.Package; using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
using System.Security.Cryptography;
namespace FrontOffice.Main.Pages; namespace FrontOffice.Main.Pages;
public partial class Index:IDisposable public partial class Index : IDisposable
{ {
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!; [Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private MobileNumberEncryptor Encryptor { get; set; }
private string? _email;
private bool _isLoadingPackages;
private List<Pack> _packs = new();
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
// ── Latest blog posts (loaded from CMS) ──
private List<BlogPostCardDto> _latestPosts = new();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
MainService.OnChangeHandler +=async () => MainService.OnChangeHandler += OnStateChanged;
// Load latest published blog posts
// اول پین‌شده‌ها، اگر کافی نبود آخرین پست‌ها اضافه شود
try
{ {
await InvokeAsync(StateHasChanged); _latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
};
// await LoadPackagesAsync(); // اگر فقط ۱ پین‌شده داریم، ۱ پست آخر هم اضافه کن
if (_latestPosts.Count < 2)
//string mobileNumber = "09387342688"; {
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
//// انکریپت کردن var existing = _latestPosts.Select(p => p.Id).ToHashSet();
//string encrypted = Encryptor.EncryptMobileNumber(mobileNumber); foreach (var post in result.Posts)
//Console.WriteLine($"Encrypted: {encrypted}"); {
if (!existing.Contains(post.Id))
//// دیکریپت کردن برای تست {
//string decrypted = Encryptor.DecryptMobileNumber(encrypted); _latestPosts.Add(post);
//Console.WriteLine($"Decrypted: {decrypted}"); if (_latestPosts.Count >= 2) break;
}
}
}
}
catch
{
_latestPosts = new();
}
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender)
{
// Init scroll-triggered fade-in animations
await JS.InvokeVoidAsync("initScrollAnimations");
// Animate stat counters
foreach (var stat in _stats)
{
await JS.InvokeVoidAsync("animateCounter", stat.ElementId, stat.Target, 2200, stat.Suffix);
}
}
if (await AuthService.IsAuthenticatedAsync()) if (await AuthService.IsAuthenticatedAsync())
{ {
if ((await AuthService.IsCompleteRegisterAsync())) if (await AuthService.IsCompleteRegisterAsync())
{ {
Navigation.NavigateTo(RouteConstants.Profile.Index); Navigation.NavigateTo(RouteConstants.Profile.Index);
} }
@@ -47,56 +69,8 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard); Navigation.NavigateTo(RouteConstants.Registration.Wizard);
} }
} }
await base.OnAfterRenderAsync(firstRender);
}
// private async Task LoadPackagesAsync() await base.OnAfterRenderAsync(firstRender);
// {
// _isLoadingPackages = true;
// try
// {
// var response = await PackageClient.GetAllPackageByFilterAsync(request: new());
// if (response?.Models?.Any() == true)
// {
// _packs = response.Models.Select(p => new Pack(
// Id: p.Id,
// Title: p.Title,
// Body: p.Description,
// Image: UrlUtility.DownloadUrl + p.ImagePath,
// Price: p.Price
// )).ToList();
// }
// else
// _packs = new List<Pack>();
// }
// catch (Exception ex)
// {
// Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error);
// // Fallback to empty list
// _packs = new List<Pack>();
// }
// finally
// {
// _isLoadingPackages = false;
// await InvokeAsync(StateHasChanged);
// }
// }
private void JoinWaitlist()
{
if (string.IsNullOrWhiteSpace(_email))
{
Snackbar.Add("لطفاً ایمیل معتبر وارد کنید.", Severity.Warning);
return;
}
Snackbar.Add("به لیست انتظار «کارا بازار سلامت» اضافه شدید.", Severity.Success);
_email = string.Empty;
}
private void NavigateToPackage(long packageId)
{
Navigation.NavigateTo($"{RouteConstants.Package.Detail}{packageId}");
} }
private void NavigateToRegistrationWizard() private void NavigateToRegistrationWizard()
@@ -104,25 +78,77 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard); Navigation.NavigateTo(RouteConstants.Registration.Wizard);
} }
private record Pack(long Id, string Title, string Body, string Image, long Price); private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features); private async void OnStateChanged()
{
await InvokeAsync(StateHasChanged);
}
private record QA(string Q, string A); // ── Trust badges ──
private readonly List<(string Icon, string Text)> _trustBadges = new()
{
(Icons.Material.Outlined.Timer, "ثبت‌نام زیر ۲ دقیقه"),
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
};
// ── How it works steps ──
private readonly List<(string Title, string Desc)> _steps = new()
{
("ثبت‌نام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظه‌ای دریافت کنید."),
};
// ── Feature cards data (6 cards) ──
private readonly List<(string Icon, string Title, string Desc, int Delay)> _features = new()
{
(Icons.Material.Outlined.VerifiedUser, "ثبت‌نام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
(Icons.Material.Outlined.StarBorder, "پاداش‌های شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.", 100),
(Icons.Material.Outlined.Devices, "طراحی واکنش‌گرا", "تجربه‌ای روان در موبایل و دسکتاپ.", 200),
(Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی شبکه و مدیریت تیم‌های فروش.", 300),
(Icons.Material.Outlined.Insights, "گزارش‌های لحظه‌ای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحله‌ای.", 500),
};
// ── Stats data (animated counters) ──
private readonly List<StatItem> _stats = new()
{
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپ‌تایم سرویس"),
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
};
// ── Testimonials ──
private readonly List<TestimonialItem> _testimonials = new()
{
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.", "شرکت سینا نت", "مدیر عملیات", 0),
new("شجره‌نامه بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
new("سادگی ثبت‌نام و شفافیت پاداش‌ها مهم‌ترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
};
// ── FAQ data ──
private readonly List<QA> _faqs = new() private readonly List<QA> _faqs = new()
{ {
new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."), new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."), new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."), new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."), new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
}; };
// ── Records ──
private record QA(string Q, string A);
private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label);
private record TestimonialItem(string Quote, string Name, string Role, int Delay);
public void Dispose() public void Dispose()
{ {
MainService.OnChangeHandler -=async () => MainService.OnChangeHandler -= OnStateChanged;
{
await InvokeAsync(StateHasChanged);
};
} }
} }
@@ -6,14 +6,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="آمار باشگاه توسعه دهندگان" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">آمار باشگاه توسعه دهندگان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState Message="در حال دریافت آمار..." />
} }
else if (_statistics is not null) else if (_statistics is not null)
{ {
@@ -3,16 +3,7 @@
<PageTitle>پکیج‌های من</PageTitle> <PageTitle>پکیج‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Breadcrumb --> <PageHeader Title="پکیج‌های من" BackHref="@RouteConstants.Profile.Index" />
<MudBreadcrumbs Items="_breadcrumbItems" Class="mb-4" />
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های من</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
وضعیت پکیج‌های خریداری شده و عضویت باشگاه مشتریان
</MudText>
</MudStack>
@if (_isLoading) @if (_isLoading)
{ {
@@ -3,13 +3,7 @@
<PageTitle>پکیج‌ها</PageTitle> <PageTitle>پکیج‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Header --> <PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" />
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های سرمایه‌گذاری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید.
</MudText>
</MudStack>
@if (_isLoading) @if (_isLoading)
{ {
+15 -16
View File
@@ -4,26 +4,25 @@
@if (_isLoading) @if (_isLoading)
{ {
<MudStack AlignItems="AlignItems.Center" Class="py-16"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" /> <LoadingState Message="در حال بارگذاری..." />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری...</MudText> </MudContainer>
</MudStack>
} }
else if (_package == null) else if (_package == null)
{ {
<MudStack AlignItems="AlignItems.Center" Class="py-16"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" /> <EmptyState Icon="@Icons.Material.Filled.Error"
<MudText Typo="Typo.h5" Class="mt-2">پکیج یافت نشد</MudText> Title="پکیج یافت نشد"
<MudText Typo="Typo.body2" Class="mud-text-secondary">پکیج مورد نظر وجود ندارد یا حذف شده است.</MudText> Description="پکیج مورد نظر وجود ندارد یا حذف شده است."
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)"> ActionText="بازگشت به صفحه اصلی"
بازگشت به صفحه اصلی ActionHref="@RouteConstants.Main.MainPage" />
</MudButton> </MudContainer>
</MudStack>
} }
else else
{ {
<!-- Breadcrumb --> <!-- Breadcrumb -->
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-4">
<PageHeader Title="جزئیات پکیج" BackHref="@RouteConstants.Package.List" />
<MudBreadcrumbs Items="_breadcrumbItems" /> <MudBreadcrumbs Items="_breadcrumbItems" />
</MudContainer> </MudContainer>
@@ -36,9 +35,9 @@ else
<MudStack Class="mb-4"> <MudStack Class="mb-4">
<!-- Package Image --> <!-- Package Image -->
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);"> <MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_package.Image" <AppImage Path="@_package.Image"
Alt="@_package.Title" Alt="@_package.Title"
Height="250" ImgHeight="250"
ObjectFit="ObjectFit.Cover" ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" ObjectPosition="ObjectPosition.Center"
Style="width:100%" Style="width:100%"
@@ -228,9 +227,9 @@ else
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 d-flex flex-column h-100 cursor-pointer" <MudPaper Elevation="2" Class="pa-4 d-flex flex-column h-100 cursor-pointer"
OnClick="() => NavigateToPackage(relatedPackage.Id)"> OnClick="() => NavigateToPackage(relatedPackage.Id)">
<MudImage Src="@relatedPackage.Image" <AppImage Path="@relatedPackage.Image"
Alt="@relatedPackage.Title" Alt="@relatedPackage.Title"
Height="200" ImgHeight="200"
ObjectFit="ObjectFit.Cover" ObjectFit="ObjectFit.Cover"
Class="rounded-xl mb-3" /> Class="rounded-xl mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">@relatedPackage.Title</MudText> <MudText Typo="Typo.h6" Class="mb-2">@relatedPackage.Title</MudText>
@@ -74,7 +74,7 @@ public partial class PackageDetail : IDisposable
Id = packageResponse.Id, Id = packageResponse.Id,
Title = packageResponse.Title, Title = packageResponse.Title,
Body = packageResponse.Description, Body = packageResponse.Description,
Image = UrlUtility.DownloadUrl + packageResponse.ImagePath, Image = packageResponse.ImagePath ?? string.Empty,
Specifications = new List<Specification> Specifications = new List<Specification>
{ {
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group }, new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="مدیریت آدرس‌ها" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">مدیریت آدرس‌ها</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4"> <MudStack Spacing="4">
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="تغییر رمز عبور" BackHref="@RouteConstants.Profile.Settings" />
<MudText Typo="Typo.h5">تغییر رمز عبور</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Settings">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3"> <MudStack Spacing="3">
@@ -7,12 +7,12 @@
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق" Variant="Variant.Outlined" <MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق" Variant="Variant.Outlined"
Dense="true" Margin="Margin.Dense" Style="width: 90px; min-width: 90px;"> Dense="true" Margin="Margin.Dense" Style="width: 90px; min-width: 90px;">
<MudSelectItem Value="2">2 سطح</MudSelectItem> <MudSelectItem T="int" Value="2">2 سطح</MudSelectItem>
<MudSelectItem Value="3">3 سطح</MudSelectItem> <MudSelectItem T="int" Value="3">3 سطح</MudSelectItem>
<MudSelectItem Value="4">4 سطح</MudSelectItem> <MudSelectItem T="int" Value="4">4 سطح</MudSelectItem>
<MudSelectItem Value="5">5 سطح</MudSelectItem> <MudSelectItem T="int" Value="5">5 سطح</MudSelectItem>
<MudSelectItem Value="6">6 سطح</MudSelectItem> <MudSelectItem T="int" Value="6">6 سطح</MudSelectItem>
<MudSelectItem Value="15">همه</MudSelectItem> <MudSelectItem T="int" Value="15">همه</MudSelectItem>
</MudSelect> </MudSelect>
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false"> <MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
@@ -9,9 +9,9 @@
@if (!string.IsNullOrWhiteSpace(node.Avatar)) @if (!string.IsNullOrWhiteSpace(node.Avatar))
{ {
<MudAvatar Size="@GetAvatarSize(Level)"> <MudAvatar Size="@GetAvatarSize(Level)">
<MudImage ObjectFit="ObjectFit.Cover" <AppImage Path="@node.Avatar"
ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover"
Src="@node.Avatar" /> ObjectPosition="ObjectPosition.Center" />
</MudAvatar> </MudAvatar>
} }
else else
@@ -0,0 +1,64 @@
@attribute [Route(RouteConstants.Profile.Hub)]
<PageTitle>پروفایل</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack AlignItems="AlignItems.Center" Spacing="3" Class="mb-6">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="profile-hub-avatar">
<MudIcon Icon="@Icons.Material.Outlined.Person" Size="Size.Large" />
</MudAvatar>
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">
@($"{_userProfile.FirstName} {_userProfile.LastName}")
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText>
</MudStack>
</MudStack>
<MudStack Spacing="2">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Person" Color="Color.Primary" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نام، کد ملی و تاریخ تولد</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.LocationOn" Color="Color.Success" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های تحویل</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Settings" Color="Color.Warning" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">تنظیمات</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تغییر رمز و تنظیمات حساب</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
</MudStack>
</MudContainer>
@@ -0,0 +1,34 @@
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Profile;
public partial class Hub
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await LoadUserProfile();
}
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
}
catch
{
_userProfile = new GetUserResponse();
}
StateHasChanged();
}
}
+183 -245
View File
@@ -5,299 +5,237 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="4"> <MudGrid Spacing="4">
<!-- Profile Header -->
@* ═══════════════════════════════════════════════
1. PROFILE HEADER — Modern gradient card
═══════════════════════════════════════════════ *@
<MudItem xs="12"> <MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6"> <MudPaper Elevation="0" Class="dash-header-card pa-5 pa-md-6 rounded-xl">
<MudStack Spacing="4"> <MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center"> <MudAvatar Size="Size.Large" Class="dash-avatar">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center"> <MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" />
<MudAvatar Size="Size.Large" Color="Color.Primary"> </MudAvatar>
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" /> <MudStack Spacing="0" Class="flex-grow-1">
</MudAvatar> <MudText Typo="Typo.h5" Class="dash-hero-name">
<div> @($"{_userProfile.FirstName} {_userProfile.LastName}")
<MudText Typo="Typo.h5">@($"{_userProfile.FirstName} {_userProfile.LastName}")</MudText> </MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText> <MudText Typo="Typo.body2" Class="dash-hero-sub">
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())</MudText> @(_userProfile.Mobile)
</div> </MudText>
</MudStack> <MudText Typo="Typo.caption" Class="dash-hero-hint">
<MudSpacer/> عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())
</MudText>
<MudButton Variant="Variant.Outlined" Href="https://dayadiamond.ir/" >سامانه دایا</MudButton>
</MudStack> </MudStack>
<MudButton Variant="Variant.Filled"
Class="rounded-pill dash-hero-btn"
<!-- Referral Code Section --> Href="https://dayadiamond.ir/"
<MudDivider /> StartIcon="@Icons.Material.Filled.Diamond">
سامانه دایا
@if (CanShowReferralLink) </MudButton>
{
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Class="flex-grow-1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode" />
</MudStack>
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success">@(_copyMessage)</MudText>
}
</MudStack>
}
else
{
<MudPaper Class="my-3 pa-3 border border-secondary">
<MudStack Spacing="3" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.h6" Align="Align.Center" Color="Color.Info">
لینک دعوت شما هنوز فعال نشده است
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
برای فعال‌سازی لینک دعوت و دعوت دوستان خود به جمع مشتریان ویژه کارابازار، کافی است ابتدا <strong>پکیج پایه ۵۶ میلیونی</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">
از مزایای ویژه عضویت در باشگاه مشتریان کارابازار سلامت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">
شروع فرآیند تامین اعتبار
</MudButton>
</MudStack>
</MudPaper>
}
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<!-- Wallet Highlight Row --> @* ═══════════════════════════════════════════════
2. WALLET STRIP — Compact inline stats
═══════════════════════════════════════════════ *@
<MudItem xs="12"> <MudItem xs="12">
<MudPaper Elevation="3" Class="pa-4 rounded-lg gradient-border"> <MudPaper Elevation="1" Class="pa-2 pa-md-4 rounded-xl">
<MudGrid Spacing="2" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceAround" AlignItems="AlignItems.Center" Class="flex-wrap wallet-strip">
<MudItem xs="12" sm="6" md="4"> <div class="wallet-strip-item">
<MudStack> <div class="wallet-strip-label">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText> <MudIcon Icon="@Icons.Material.Outlined.CreditCard" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletCredit</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">اعتباری</MudText>
</MudStack> </div>
</MudItem> <MudText Typo="Typo.subtitle2" Color="Color.Primary">@_walletCredit</MudText>
<MudItem xs="12" sm="6" md="4"> </div>
<MudStack> <MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی تخفیف باشگاه</MudText> <div class="wallet-strip-item">
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletDiscount</MudText> <div class="wallet-strip-label">
</MudStack> <MudIcon Icon="@Icons.Material.Outlined.Discount" Size="Size.Small" Color="Color.Success" />
</MudItem> <MudText Typo="Typo.caption" Class="mud-text-secondary">تخفیف باشگاه</MudText>
<MudItem xs="12" sm="6" md="4"> </div>
<MudStack> <MudText Typo="Typo.subtitle2" Color="Color.Success">@_walletDiscount</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی پاداش تیمی</MudText> </div>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletNetwork</MudText> <MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
</MudStack> <div class="wallet-strip-item">
</MudItem> <div class="wallet-strip-label">
<MudItem xs="12" md="4" Class="d-flex justify-end"> <MudIcon Icon="@Icons.Material.Outlined.Groups" Size="Size.Small" Color="Color.Warning" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AccountBalanceWallet" Href="@RouteConstants.Profile.Wallet">جزئیات کیف پول</MudButton> <MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش تیمی</MudText>
</MudItem> </div>
</MudGrid> <MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
Href="@RouteConstants.Profile.Wallet" EndIcon="@Icons.Material.Filled.ArrowBack">
کیف پول
</MudButton>
</MudStack>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<!-- Profile Content as tiles --> @* ═══════════════════════════════════════════════
3. REFERRAL SECTION
═══════════════════════════════════════════════ *@
<MudItem xs="12"> <MudItem xs="12">
<MudPaper Elevation="4" Class="pa-4"> @if (CanShowReferralLink)
<MudGrid Spacing="3"> {
<MudItem xs="6" sm="6" md="3"> <MudPaper Elevation="1" Class="pa-5 rounded-xl">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None" Class="tile-link"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
<MudCard Elevation="1" Class="rounded-lg profile-tile"> <MudIcon Icon="@Icons.Material.Filled.Share" Color="Color.Primary" />
<MudCardContent Class="d-flex flex-column align-center pa-4"> <MudText Typo="Typo.h6">کد دعوت شما</MudText>
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" Color="Color.Primary" /> <MudSpacer />
<MudText Typo="Typo.subtitle1" Class="mt-2">اطلاعات شخصی</MudText> <MudButton Variant="Variant.Filled"
<MudText Typo="Typo.caption" Class="mud-text-secondary">نمایش و ویرایش اطلاعات</MudText> Color="Color.Primary"
</MudCardContent> Size="Size.Small"
</MudCard> Class="rounded-pill"
</MudLink> StartIcon="@Icons.Material.Filled.Share"
</MudItem> OnClick="ShareReferralCode">
<MudItem xs="6" sm="6" md="3"> اشتراک‌گذاری
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None" Class="tile-link"> </MudButton>
<MudCard Elevation="1" Class="rounded-lg profile-tile"> </MudStack>
<MudCardContent Class="d-flex flex-column align-center pa-4"> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Large" Color="Color.Primary" /> این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
<MudText Typo="Typo.subtitle1" Class="mt-2">آدرس‌ها</MudText> </MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های شما</MudText> <MudTextField @bind-Value="_userProfile.ReferralCode"
</MudCardContent> Label="کد دعوت"
</MudCard> Variant="Variant.Outlined"
</MudLink> ReadOnly="true"
</MudItem> Adornment="Adornment.End"
<MudItem xs="6" sm="6" md="3"> AdornmentIcon="@Icons.Material.Filled.ContentCopy"
<MudLink Href="@RouteConstants.Profile.Tree" Underline="Underline.None" Class="tile-link"> OnAdornmentClick="CopyReferralCode"
<MudCard Elevation="1" Class="rounded-lg profile-tile"> Class="rounded-lg" />
<MudCardContent Class="d-flex flex-column align-center pa-4"> @if (!string.IsNullOrWhiteSpace(_copyMessage))
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Large" Color="Color.Primary" /> {
<MudText Typo="Typo.subtitle1" Class="mt-2">شجره‌نامه</MudText> <MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ساختار تیم</MudText> }
</MudCardContent> </MudPaper>
</MudCard> }
</MudLink> else
</MudItem> {
<MudItem xs="6" sm="6" md="3"> <MudPaper Elevation="0" Class="pa-6 rounded-xl text-center" Style="border:2px dashed var(--mud-palette-primary); background:rgba(99,102,241,.04);">
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None" Class="tile-link"> <MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Primary" Class="mb-2" Style="font-size:2.5rem;" />
<MudCard Elevation="1" Class="rounded-lg profile-tile"> <MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-2">
<MudCardContent Class="d-flex flex-column align-center pa-4"> لینک دعوت شما هنوز فعال نشده است
<MudIcon Icon="@Icons.Material.Filled.Settings" Size="Size.Large" Color="Color.Primary" /> </MudText>
<MudText Typo="Typo.subtitle1" Class="mt-2">تنظیمات حساب</MudText> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
<MudText Typo="Typo.caption" Class="mud-text-secondary">اعلان‌ها و تنظیمات</MudText> برای فعال‌سازی لینک دعوت، ابتدا <strong>پکیج پایه</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudCardContent> </MudText>
</MudCard> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
</MudLink> از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudItem> </MudText>
<MudItem xs="6" sm="6" md="3"> <MudButton Variant="Variant.Filled"
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="tile-link"> Color="Color.Primary"
<MudCard Elevation="1" Class="rounded-lg profile-tile"> Size="Size.Large"
<MudCardContent Class="d-flex flex-column align-center pa-4"> Class="rounded-pill"
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Color="Color.Primary" /> StartIcon="@Icons.Material.Filled.ShoppingCart"
<MudText Typo="Typo.subtitle1" Class="mt-2">فروشگاه</MudText> OnClick="OpenPurchaseOptions">
<MudText Typo="Typo.caption" Class="mud-text-secondary">مشاهده و خرید محصولات</MudText> شروع فرآیند تامین اعتبار
</MudCardContent> </MudButton>
</MudCard> </MudPaper>
</MudLink> }
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Wallet" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">کیف پول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.WithdrawalRequests" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.RequestPage" Size="Size.Large" Color="Color.Warning" />
<MudText Typo="Typo.subtitle1" Class="mt-2">درخواست برداشت</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ثبت و پیگیری برداشت</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Club.Membership" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Size="Size.Large" Color="Color.Secondary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">باشگاه مشتریان</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضویت و مزایا</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Network.Statistics" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Groups" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.subtitle1" Class="mt-2">آمار باشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">آمار جزئیات باشگاه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Commission.Dashboard" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Payments" Size="Size.Large" Color="Color.Success" />
<MudText Typo="Typo.subtitle1" Class="mt-2">پاداش</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">درآمد و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem> </MudItem>
@* ═══════════════════════════════════════════════
3.5 LATEST BLOG BANNER
═══════════════════════════════════════════════ *@
@if (_latestPost is not null)
{
<MudItem xs="12">
<MudLink Href="@($"/blog/{_latestPost.Slug}")" Underline="Underline.None">
<MudPaper Elevation="1" Class="rounded-xl dash-blog-banner overflow-hidden">
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2" Spacing="2">
@if (!string.IsNullOrWhiteSpace(_latestPost.ThumbnailUrl))
{
<div style="width:72px;height:72px;min-width:72px;border-radius:10px;overflow:hidden;">
<AppImage Path="@_latestPost.ThumbnailUrl" Alt="@_latestPost.Title"
ObjectFit="ObjectFit.Cover" Style="width:100%;height:100%;" />
</div>
}
else
{
<MudAvatar Rounded="true" Size="Size.Large" Color="Color.Primary" Variant="Variant.Outlined">
<MudIcon Icon="@Icons.Material.Outlined.Article" />
</MudAvatar>
}
<MudStack Spacing="0" Class="flex-grow-1" Style="min-width:0;">
<MudText Typo="Typo.caption" Color="Color.Primary" Style="font-size:0.7rem;">جدیدترین مطلب</MudText>
<MudText Typo="Typo.body2" Class="dash-blog-title" Style="font-size:0.85rem;">@_latestPost.Title</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudPaper>
</MudLink>
</MudItem>
}
@* ═══════════════════════════════════════════════
4. QUICK ACCESS TILES — Modern grid
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudText Typo="Typo.h6" Class="mb-3">دسترسی سریع</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var tile in _dashTiles)
{
<MudItem xs="6" sm="4" md="3">
<MudLink Href="@tile.Href" Underline="Underline.None" Class="tile-link">
<MudPaper Elevation="0" Class="pa-4 rounded-xl dash-tile text-center">
<MudAvatar Size="Size.Medium" Class="mx-auto mb-2" Style="@tile.AvatarStyle">
<MudIcon Icon="@tile.Icon" Size="Size.Medium" />
</MudAvatar>
<MudText Typo="Typo.subtitle2">@tile.Title</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tile.Subtitle</MudText>
</MudPaper>
</MudLink>
</MudItem>
}
</MudGrid>
</MudItem>
</MudGrid> </MudGrid>
</MudContainer> </MudContainer>
<!-- Purchase Options Bottom Sheet --> @* ── Purchase Options Dialog ── *@
<MudDialog @bind-Visible="_showPurchaseBottomSheet"> <MudDialog @bind-Visible="_showPurchaseBottomSheet">
<DialogContent> <DialogContent>
<MudStack Spacing="3" Class="pa-2"> <MudStack Spacing="3" Class="pa-2">
<MudText Typo="Typo.h6" Align="Align.Center"> <MudText Typo="Typo.h6" Align="Align.Center">
خرید پکیج پایه ۵۶ میلیون تومان خرید پکیج پایه ۵۶ میلیون تومان
</MudText> </MudText>
<MudDivider /> <MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">
برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید: برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید:
</MudText> </MudText>
<!-- Direct Payment Option -->
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-primary);"> <MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-primary);">
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" /> <MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText> <MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
</MudStack> </MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
پرداخت آنی از طریق درگاه بانکی <MudButton Variant="Variant.Filled" Disabled="true" Color="Color.Primary" FullWidth="true"
</MudText> StartIcon="@Icons.Material.Filled.Payment" OnClick="DirectPayment">
<MudButton Variant="Variant.Filled" Disabled="true"
Color="Color.Primary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="DirectPayment">
پرداخت با کارت بانکی(بزودی) پرداخت با کارت بانکی(بزودی)
</MudButton> </MudButton>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
<!-- Daya Loan Option -->
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-tertiary);"> <MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-tertiary);">
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" /> <MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText> <MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
</MudStack> </MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">تأمین اعتبار از طریق خرید توکن الماس و دریافت وام</MudText>
تأمین اعتبار از طریق خرید توکن الماس و دریافت وام <MudButton Variant="Variant.Filled" Color="Color.Tertiary" FullWidth="true"
</MudText> StartIcon="@Icons.Material.Filled.AccountBalance" OnClick="DayaLoanPayment">
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalance"
OnClick="DayaLoanPayment">
تأمین اعتبار الماسی تأمین اعتبار الماسی
</MudButton> </MudButton>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
<MudButton Variant="Variant.Text" <MudButton Variant="Variant.Text" Color="Color.Default" FullWidth="true" OnClick="ClosePurchaseOptions">
Color="Color.Default"
FullWidth="true"
OnClick="ClosePurchaseOptions">
بستن بستن
</MudButton> </MudButton>
</MudStack> </MudStack>
@@ -22,10 +22,11 @@ public partial class Index
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!; [Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private GetUserResponse _userProfile = new(); private GetUserForCustomerResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new(); private BlogPostCardDto? _latestPost;
private readonly UpdateUserRequestValidator _personalValidator = new(); private UpdateCustomerProfileRequest _updateUserRequest = new();
private MudForm? _personalForm; private MudForm? _personalForm;
@@ -45,6 +46,22 @@ public partial class Index
private bool _showPurchaseBottomSheet; private bool _showPurchaseBottomSheet;
private bool _isProcessingPayment; private bool _isProcessingPayment;
// Dashboard Tiles
private readonly List<DashTile> _dashTiles = new()
{
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرس‌ها", "مدیریت آدرس‌ها", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجره‌نامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
new(RouteConstants.Commission.Dashboard, Icons.Material.Filled.Payments, "پاداش", "درآمد و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
};
private record DashTile(string Href, string Icon, string Title, string Subtitle, string AvatarStyle);
// Address management // Address management
private List<CustomerAddressModel> _addresses = new(); private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses; private bool _isLoadingAddresses;
@@ -64,6 +81,7 @@ public partial class Index
await LoadUserProfile(); await LoadUserProfile();
await LoadAddresses(); await LoadAddresses();
await LoadWallet(); await LoadWallet();
await LoadLatestPost();
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده // نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
await CheckAndShowClubContractModal(); await CheckAndShowClubContractModal();
@@ -134,19 +152,47 @@ public partial class Index
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task LoadLatestPost()
{
try
{
var posts = await BlogPostService.GetFeaturedPostsAsync(1);
if (!posts.Any())
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 1);
posts = result.Posts;
}
_latestPost = posts.FirstOrDefault();
}
catch
{
_latestPost = null;
}
StateHasChanged();
}
private async Task LoadUserProfile() private async Task LoadUserProfile()
{ {
try try
{ {
_userProfile = await UserContract.GetUserAsync(request: new()); _userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>(); _updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null) if (_userProfile.BirthDate != null)
{
_date = _userProfile.BirthDate.ToDateTime(); _date = _userProfile.BirthDate.ToDateTime();
_updateUserRequest.BirthDate = _userProfile.BirthDate;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
// Handle the case when user is not authenticated or API fails // Handle the case when user is not authenticated or API fails
_userProfile = new GetUserResponse(); _userProfile = new GetUserForCustomerResponse();
} }
StateHasChanged(); StateHasChanged();
} }
@@ -168,7 +214,7 @@ public partial class Index
if (_date != null) if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp(); _updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(request: _updateUserRequest); await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
await LoadUserProfile(); await LoadUserProfile();
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
} }
@@ -3,6 +3,7 @@
@using Blazored.LocalStorage @using Blazored.LocalStorage
@using CMSMicroservice.Protobuf.Protos.Package @using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.User @using CMSMicroservice.Protobuf.Protos.User
@inject IJSRuntime JS
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle> <PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
@@ -37,7 +38,7 @@
</MudText> </MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6" <MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile"> OnClick="GoBack">
بازگشت به پروفایل بازگشت به پروفایل
</MudButton> </MudButton>
} }
@@ -48,7 +49,7 @@
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText> <MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6" <MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile"> OnClick="GoBack">
بازگشت به پروفایل بازگشت به پروفایل
</MudButton> </MudButton>
@@ -173,4 +174,6 @@
} }
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
} }
@@ -4,13 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="اطلاعات شخصی" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">اطلاعات شخصی</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)"> <MudForm @ref="_personalForm" Model="_updateUserRequest">
<MudGrid Spacing="3"> <MudGrid Spacing="3">
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName" <MudTextField @bind-Value="_updateUserRequest.FirstName"
@@ -1,8 +1,6 @@
using CMSMicroservice.Protobuf.Protos.User; using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Validator.User;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
using Severity = MudBlazor.Severity; using Severity = MudBlazor.Severity;
@@ -13,9 +11,8 @@ public partial class Personal : ComponentBase
{ {
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new(); private GetUserForCustomerResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new(); private UpdateCustomerProfileRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private MudForm? _personalForm; private MudForm? _personalForm;
private DateTime? _date; private DateTime? _date;
@@ -30,8 +27,14 @@ public partial class Personal : ComponentBase
{ {
try try
{ {
_userProfile = await UserContract.GetUserAsync(new()); _userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>(); _updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null) if (_userProfile.BirthDate != null)
_date = _userProfile.BirthDate.ToDateTime(); _date = _userProfile.BirthDate.ToDateTime();
} }
@@ -56,7 +59,7 @@ public partial class Personal : ComponentBase
if (_date != null) if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp(); _updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(_updateUserRequest); await UserContract.UpdateCustomerProfileAsync(_updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="تنظیمات حساب" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">تنظیمات حساب</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4"> <MudStack Spacing="4">
@@ -1,5 +1,4 @@
using CMSMicroservice.Protobuf.Protos.User; using CMSMicroservice.Protobuf.Protos.User;
using Mapster;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
@@ -16,8 +15,18 @@ public partial class Settings : ComponentBase
{ {
try try
{ {
var user = await UserContract.GetUserAsync(new()); var user = await UserContract.GetUserForCustomerAsync(new());
_request = user.Adapt<UpdateUserRequest>(); _request = new UpdateUserRequest
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
NationalCode = user.NationalCode,
EmailNotifications = user.EmailNotifications,
SmsNotifications = user.SmsNotifications,
PushNotifications = user.PushNotifications,
};
} }
catch catch
{ {
@@ -5,10 +5,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="شجره‌نامه" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">شجره‌نامه</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<OrganizationChart /> <OrganizationChart />
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="کیف پول" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.h5">کیف پول</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudGrid Spacing="2"> <MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <PageHeader Title="درخواست‌های برداشت" BackHref="@RouteConstants.Profile.Wallet" />
<MudText Typo="Typo.h5">درخواست‌های برداشت</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Wallet">بازگشت به کیف پول</MudButton>
</MudStack>
<!-- فرم درخواست برداشت جدید --> <!-- فرم درخواست برداشت جدید -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -100,7 +97,7 @@
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <LoadingState />
} }
else if (!_withdrawals.Any()) else if (!_withdrawals.Any())
{ {
+68 -55
View File
@@ -6,10 +6,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="px-1"> <MudContainer MaxWidth="MaxWidth.Large" Class="px-1">
<MudGrid Spacing="4" Justify="Justify.Center"> <MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="5" Class="d-none d-sm-block"> <MudItem xs="12" md="5" Class="d-none d-sm-block">
<MudStack Spacing="2" Class="mb-6"> <MudStack Spacing="3" Class="mb-6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ثبت‌نام سه <MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small" Class="rounded-pill">
مرحله‌ای</MudChip> ثبت‌نام سه مرحله‌ای
<MudText Typo="Typo.h3" Class="mb-2"> </MudChip>
<MudText Typo="Typo.h3" Class="mb-1">
فقط در چند دقیقه حساب خود را فعال کنید فقط در چند دقیقه حساب خود را فعال کنید
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary"> <MudText Typo="Typo.body1" Class="mud-text-secondary">
@@ -18,34 +19,47 @@
</MudText> </MudText>
</MudStack> </MudStack>
<MudPaper Elevation="1" Class="pa-4 rounded-xl gradient-border"> <MudPaper Elevation="0" Class="pa-5 rounded-xl" Style="border:1px solid var(--mud-palette-divider);">
<MudStack Spacing="2"> <MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Primary" /> <MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.Verified" Size="Size.Small" />
</MudAvatar>
<MudText Typo="Typo.subtitle1">مزایای ثبت‌نام آنلاین</MudText> <MudText Typo="Typo.subtitle1">مزایای ثبت‌نام آنلاین</MudText>
</MudStack> </MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudStack Spacing="2">
• تایید سریع و آنلاین شماره موبایل<br /> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
• تکمیل اطلاعات هویتی بدون مراجعه حضوری<br /> <MudIcon Icon="@Icons.Material.Outlined.PhoneAndroid" Size="Size.Small" Color="Color.Primary" />
• دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق <MudText Typo="Typo.body2" Class="mud-text-secondary">تایید سریع و آنلاین شماره موبایل</MudText>
</MudText> </MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Badge" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">تکمیل اطلاعات هویتی بدون مراجعه حضوری</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Description" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق</MudText>
</MudStack>
</MudStack>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="7"> <MudItem xs="12" md="7">
<MudPaper Elevation="3" Class="pa-6 wizard-card"> <MudPaper Elevation="1" Class="pa-5 pa-md-6 wizard-card">
@if (_completed) @if (_completed)
{ {
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center"> <MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center py-6">
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" /> <MudAvatar Size="Size.Large" Color="Color.Success" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.CheckCircle" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText> <MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary"> <MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width:400px;">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید. را دنبال کنید.
</MudText> </MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center"> <MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
اصلی</MudButton> اصلی</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" <MudButton Variant="Variant.Outlined" Color="Color.Primary"
@@ -56,43 +70,45 @@
} }
else else
{ {
<MudStack Spacing="3" > <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h4">مراحل ثبت‌نام</MudText> <MudText Typo="Typo.h5">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله <MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" Class="rounded-pill">
مرحله @(_activeStep + 1) از ۳
</MudChip> </MudChip>
</MudStack> </MudStack>
@if (_isSubmitting) @if (_isSubmitting)
{ {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="rounded" />
} }
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4 " id="registration-stepper"> <MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4" id="registration-stepper">
<ChildContent> <ChildContent>
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone"> <MudStep Label="تأیید موبایل" Icon="@Icons.Material.Outlined.PhoneAndroid">
@* Inline AuthDialog with captcha enabled *@ @* Inline AuthDialog with captcha enabled *@
<AuthDialog @ref="_authDialog" InlineMode="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" /> <AuthDialog @ref="_authDialog" InlineMode="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
</MudStep> </MudStep>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge"> <MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Outlined.Badge">
<MudForm @ref="_stepTwoForm"> <MudForm @ref="_stepTwoForm">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true" <MudStack Spacing="3">
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" /> <MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true" @bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
@bind-Value="_model.LastName" For="@(() => _model.LastName)" /> <MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true" @bind-Value="_model.LastName" For="@(() => _model.LastName)" />
MaxLength="10" @bind-Value="_model.NationalCode" <MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
For="@(() => _model.NationalCode)" InputType="InputType.Number" /> MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
</MudStack>
</MudForm> </MudForm>
</MudStep> </MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule"> <MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Outlined.Rule">
<MudForm @ref="_stepThreeForm"> <MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3"> <MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3 rounded-lg">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره تایید را فعال نمایید.
کنید.
</MudAlert> </MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3"> <MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText> <MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
@@ -106,47 +122,44 @@
</MudList> </MudList>
</MudPaper> </MudPaper>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2"> <MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-lg mb-3"
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Outlined.Download" Disabled="_isSubmitting"
StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting" OnClick="DownloadContract">
OnClick="DownloadContract"> دانلود قرارداد نمونه (PDF)
دانلود/پرینت قرارداد نمونه </MudButton>
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF</MudText>
</MudStack>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success" <MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Primary"
For="@(() => _model.AcceptTerms)" For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" /> Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm> </MudForm>
</MudStep> </MudStep>
</ChildContent> </ChildContent>
<ActionContent Context="stepper"> <ActionContent Context="stepper">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mt-2">
@if (_isAuthenticated && _activeStep == 1) @if (_isAuthenticated && _activeStep == 1)
{ {
<MudButton Variant="Variant.Text" Color="Color.Secondary" <MudButton Variant="Variant.Text" Color="Color.Error" Size="Size.Small"
OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton> OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton>
} }
else else
{ {
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_activeStep == 0 || _isSubmitting"
Disabled="_activeStep == 0 || _isSubmitting" OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
} }
<MudSpacer/> <MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
Disabled="_isSubmitting" Disabled="_isSubmitting"
OnClick="@(async () => { await GoNextAsync(); })"> OnClick="@(async () => { await GoNextAsync(); })">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
@_nextButtonText @_nextButtonText
</MudButton> </MudButton>
</MudStack> </MudStack>
</ActionContent> </ActionContent>
</MudStepper> </MudStepper>
</MudStack> </MudStack>
} }
</MudPaper> </MudPaper>
@@ -23,13 +23,14 @@ public partial class RegisterWizard
private bool _completed; private bool _completed;
private AuthDialog? _authDialog; private AuthDialog? _authDialog;
private MudStepper? _mudStepper; private MudStepper? _mudStepper;
private UpdateUserRequest _updateUserRequest = new(); private UpdateCustomerProfileRequest _updateUserRequest = new();
private bool _isAuthenticated; private bool _isAuthenticated;
private CancellationTokenSource? _operationCts; private CancellationTokenSource? _operationCts;
private UserAuthInfo _userAuthInfo; private UserAuthInfo _userAuthInfo;
private Guid signGuid = Guid.NewGuid(); private Guid signGuid = Guid.NewGuid();
private const string TokenStorageKey = "auth:token"; private const string TokenStorageKey = "auth:token";
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
private bool _initialDataLoaded;
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private IDeviceDetector _deviceDetector { get; set; } = default!; [Inject] private IDeviceDetector _deviceDetector { get; set; } = default!;
@@ -55,6 +56,7 @@ public partial class RegisterWizard
if (_userAuthInfo.IsSignMainContract) if (_userAuthInfo.IsSignMainContract)
{ {
Navigation.NavigateTo(RouteConstants.Profile.Index); Navigation.NavigateTo(RouteConstants.Profile.Index);
return;
} }
if (_activeStep == 0) if (_activeStep == 0)
@@ -63,8 +65,9 @@ public partial class RegisterWizard
_activeStep = 1; _activeStep = 1;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
else if (_activeStep == 1) else if (_activeStep == 1 && !_initialDataLoaded)
{ {
_initialDataLoaded = true;
try try
{ {
var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest()); var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest());
@@ -95,7 +98,6 @@ public partial class RegisterWizard
} }
} }
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
@@ -355,7 +357,7 @@ public partial class RegisterWizard
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish(); _updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();
await UserContract.UpdateUserAsync(request: _updateUserRequest); await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
return true; return true;
} }
+9 -9
View File
@@ -5,7 +5,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudText Typo="Typo.h4">سبد خرید</MudText> <PageHeader Title="سبد خرید" BackHref="@RouteConstants.Store.Products" />
@if (CartData.Items.Count == 0) @if (CartData.Items.Count == 0)
{ {
@@ -30,8 +30,8 @@
<MudTd> <MudTd>
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title" <AppImage Path="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
Width="64" Height="64" Class="product-thumb" /> ImgWidth="64" ImgHeight="64" Class="product-thumb" />
<MudText>@context.Title</MudText> <MudText>@context.Title</MudText>
</MudStack> </MudStack>
@if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description)) @if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description))
@@ -86,8 +86,8 @@
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title" <AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="50" Height="50" Class="rounded-circle" /> ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText> <MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack> </MudStack>
@if (item.Discount > 0) @if (item.Discount > 0)
@@ -154,8 +154,8 @@
</MudStack> </MudStack>
} }
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText> <MudText Typo="Typo.h6" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText> <MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack> </MudStack>
</MudStack> </MudStack>
@@ -185,8 +185,8 @@
} }
<MudDivider Class="my-1" /> <MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText> <MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText> <MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack> </MudStack>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
@@ -66,7 +66,7 @@ public partial class Cart : ComponentBase, IDisposable
private long VATAmount => VAT.CalculateVAT(CartData.Total); private long VATAmount => VAT.CalculateVAT(CartData.Total);
private static string GetProductImageUrl(string? imageUrl) private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose() public void Dispose()
{ {
@@ -5,9 +5,9 @@
<PageTitle>دسته‌بندی‌ها</PageTitle> <PageTitle>دسته‌بندی‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="دسته‌بندی محصولات" BackHref="@RouteConstants.Store.Products" />
<MudPaper Elevation="1" Class="pa-4 mb-4 rounded-lg"> <MudPaper Elevation="1" Class="pa-4 mb-4 rounded-lg">
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudText Typo="Typo.h5">دسته‌بندی محصولات</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">
از بین درخت دسته‌بندی‌ها انتخاب کنید تا محصولات آن دسته را مشاهده نمایید. از بین درخت دسته‌بندی‌ها انتخاب کنید تا محصولات آن دسته را مشاهده نمایید.
</MudText> </MudText>
@@ -5,6 +5,7 @@
<PageTitle>خلاصه خرید</PageTitle> <PageTitle>خلاصه خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="خلاصه خرید" BackHref="@RouteConstants.Store.Cart" />
<MudGrid Spacing="3"> <MudGrid Spacing="3">
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3"> <MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
@@ -30,7 +31,7 @@
@foreach (var address in _addresses) @foreach (var address in _addresses)
{ {
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)" <MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)" Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer" Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")" Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address"> @onclick="() => _selectedAddress = address">
@@ -84,8 +85,8 @@
<MudListItemText> <MudListItemText>
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title" <AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="48" Height="48" Class="rounded-circle" /> ImgWidth="48" ImgHeight="48" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText> <MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText> <MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText>
</MudStack> </MudStack>
@@ -118,8 +119,8 @@
} }
<MudDivider Class="my-1" /> <MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText> <MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText> <MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText>
</MudStack> </MudStack>
</MudStack> </MudStack>
@@ -113,5 +113,5 @@ public partial class CheckoutSummary : ComponentBase
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total); private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
private static string GetProductImageUrl(string? imageUrl) private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
} }
@@ -6,24 +6,24 @@
@if (_loading) @if (_loading)
{ {
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center"> <LoadingState />
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
</MudContainer> </MudContainer>
} }
else if (_order is null) else if (_order is null)
{ {
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert> <EmptyState Icon="@Icons.Material.Filled.SearchOff"
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Orders)">بازگشت</MudButton> Title="سفارش یافت نشد."
ActionText="بازگشت"
ActionHref="@RouteConstants.Store.Orders" />
</MudContainer> </MudContainer>
} }
else else
{ {
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="@($"جزئیات سفارش #{_order.Id}")" BackHref="@RouteConstants.Store.Orders" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudText Typo="Typo.h5">سفارش #@_order.Id</MudText>
@if (_order.PaymentDate != null) @if (_order.PaymentDate != null)
{ {
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ پرداخت: @_order.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</MudText> <MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ پرداخت: @_order.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</MudText>
@@ -46,8 +46,8 @@ else
<RowTemplate> <RowTemplate>
<MudTd> <MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle" <AppImage Path="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
Width="60" Height="60" Class="rounded-circle" /> ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
<MudText>@context.ProductTitle</MudText> <MudText>@context.ProductTitle</MudText>
</MudStack> </MudStack>
</MudTd> </MudTd>
@@ -65,8 +65,8 @@ else
<MudPaper Class="pa-3 rounded-lg" Outlined="true"> <MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle" <AppImage Path="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
Width="60" Height="60" Class="rounded-circle" /> ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
<MudText>@it.ProductTitle</MudText> <MudText>@it.ProductTitle</MudText>
</MudStack> </MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -100,15 +100,15 @@ else
</MudStack> </MudStack>
<MudDivider Class="my-1" /> <MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText> <MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(_order.VatInfo.TotalAmount)</MudText> <MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(_order.VatInfo.TotalAmount)</MudText>
</MudStack> </MudStack>
} }
else else
{ {
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText> <MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(subtotal)</MudText> <MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(subtotal)</MudText>
</MudStack> </MudStack>
} }
</MudStack> </MudStack>
@@ -45,6 +45,6 @@ public partial class OrderDetail : ComponentBase
} }
private static string GetProductImageUrl(string? imageUrl) private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : UrlUtility.DownloadUrl+imageUrl; => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
} }
@@ -4,6 +4,7 @@
<PageTitle>پیگیری سفارش</PageTitle> <PageTitle>پیگیری سفارش</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<PageHeader Title="پیگیری سفارش" BackHref="@RouteConstants.Store.Orders" />
@if (_loading) @if (_loading)
{ {
<MudStack AlignItems="AlignItems.Center"> <MudStack AlignItems="AlignItems.Center">
@@ -15,7 +16,7 @@
{ {
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert> <MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" <MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@RouteConstants.Store.Orders"> OnClick="GoBack">
بازگشت به سفارش‌ها بازگشت به سفارش‌ها
</MudButton> </MudButton>
} }
@@ -1,6 +1,7 @@
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Messages = CMSMicroservice.Protobuf.Protos; using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store; namespace FrontOffice.Main.Pages.Store;
@@ -8,6 +9,7 @@ namespace FrontOffice.Main.Pages.Store;
public partial class OrderTracking : ComponentBase public partial class OrderTracking : ComponentBase
{ {
[Inject] private OrderService OrderService { get; set; } = default!; [Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
[Parameter] public long id { get; set; } [Parameter] public long id { get; set; }
@@ -123,4 +125,6 @@ public partial class OrderTracking : ComponentBase
public bool IsCurrent { get; set; } public bool IsCurrent { get; set; }
public string Date { get; set; } = ""; public string Date { get; set; } = "";
} }
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
} }
@@ -4,12 +4,10 @@
<PageTitle>سفارشات من</PageTitle> <PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudText Typo="Typo.h4" Class="mb-3">سفارشات من</MudText> <PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
@if (_loading) @if (_loading)
{ {
<MudStack AlignItems="AlignItems.Center" Class="py-4"> <LoadingState />
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
} }
else if (_orders.Count == 0) else if (_orders.Count == 0)
{ {
@@ -6,23 +6,22 @@
@if (_loading) @if (_loading)
{ {
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center"> <LoadingState />
<MudProgressCircular Color="Color.Primary" Indeterminate="true"/>
</MudStack>
</MudContainer> </MudContainer>
} }
else if (_product is null) else if (_product is null)
{ {
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">محصول یافت نشد.</MudAlert> <EmptyState Icon="@Icons.Material.Filled.SearchOff"
<MudButton Class="mt-2" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Title="محصول یافت نشد."
OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">بازگشت ActionText="بازگشت"
</MudButton> ActionHref="@RouteConstants.Store.Products" />
</MudContainer> </MudContainer>
} }
else else
{ {
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="جزئیات محصول" BackHref="@RouteConstants.Store.Products" />
<MudGrid Spacing="3"> <MudGrid Spacing="3">
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
@if (_product is not null) @if (_product is not null)
@@ -32,7 +31,9 @@ else
<MudPaper Class="rounded-lg pa-2"> <MudPaper Class="rounded-lg pa-2">
@if (!string.IsNullOrWhiteSpace(MainImageUrl)) @if (!string.IsNullOrWhiteSpace(MainImageUrl))
{ {
<div class="rounded-lg" style="width:100%;height:360px;background-image: url('@MainImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem;color: transparent;" >@MainImageAlt</div> <AppImage Path="@MainImageUrl" Alt="@MainImageAlt"
ObjectFit="ObjectFit.Cover"
Style="width:100%; max-height:400px; border-radius:12px;" />
} }
else else
{ {
@@ -45,7 +46,7 @@ else
@foreach (var item in _galleryItems) @foreach (var item in _galleryItems)
{ {
<MudButton Variant="Variant.Text" Class="p-0" Style="min-width:0;" OnClick="() => SelectGalleryImage(item)"> <MudButton Variant="Variant.Text" Class="p-0" Style="min-width:0;" OnClick="() => SelectGalleryImage(item)">
<MudImage Src="@GetThumbnailUrl(item)" Alt="@item.Title" Class="rounded-sm" Style="@GetThumbnailStyle(item)" /> <AppImage Path="@GetThumbnailUrl(item)" Alt="@item.Title" Class="rounded-sm" Style="@GetThumbnailStyle(item)" />
</MudButton> </MudButton>
} }
</MudStack> </MudStack>
@@ -3,7 +3,18 @@
<PageTitle>محصولات</PageTitle> <PageTitle>محصولات</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="pa-2 pa-md-6"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Hero Banner -->
<MudPaper Class="store-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="dash-hero-name" />
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه کارا بازار سلامت</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="dash-hero-sub">
محصولات با کیفیت با ضمانت اصالت و ارسال سریع
</MudText>
</MudStack>
</MudPaper>
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg"> <MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center"> <MudGrid Spacing="2" AlignItems="AlignItems.Center">
@* ردیف اول: جستجو و دکمه‌ها *@ @* ردیف اول: جستجو و دکمه‌ها *@
@@ -44,25 +55,25 @@
Margin="Margin.Dense" Margin="Margin.Dense"
AnchorOrigin="Origin.BottomCenter" AnchorOrigin="Origin.BottomCenter"
AdornmentIcon="@Icons.Material.Filled.Sort"> AdornmentIcon="@Icons.Material.Filled.Sort">
<MudSelectItem Value="ProductSortOption.PriceDesc"> <MudSelectItem T="ProductSortOption" Value="ProductSortOption.PriceDesc">
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small"/> <MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small"/>
گران‌ترین گران‌ترین
</div> </div>
</MudSelectItem> </MudSelectItem>
<MudSelectItem Value="ProductSortOption.PriceAsc"> <MudSelectItem T="ProductSortOption" Value="ProductSortOption.PriceAsc">
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Size="Size.Small"/> <MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Size="Size.Small"/>
ارزان‌ترین ارزان‌ترین
</div> </div>
</MudSelectItem> </MudSelectItem>
<MudSelectItem Value="ProductSortOption.Newest"> <MudSelectItem T="ProductSortOption" Value="ProductSortOption.Newest">
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.NewReleases" Size="Size.Small"/> <MudIcon Icon="@Icons.Material.Filled.NewReleases" Size="Size.Small"/>
جدیدترین جدیدترین
</div> </div>
</MudSelectItem> </MudSelectItem>
<MudSelectItem Value="ProductSortOption.Title"> <MudSelectItem T="ProductSortOption" Value="ProductSortOption.Title">
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.SortByAlpha" Size="Size.Small"/> <MudIcon Icon="@Icons.Material.Filled.SortByAlpha" Size="Size.Small"/>
الفبایی الفبایی
+8 -3
View File
@@ -8,15 +8,19 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#7c4dff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<base href="~/" /> <base href="~/" />
<link href="css/site.css" rel="stylesheet" />
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<component type="typeof(HeadOutlet)" render-mode="Server" /> <component type="typeof(HeadOutlet)" render-mode="Server" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<!-- Ensure latest MudBlazor CSS is used (cache-busting) --> <!-- MudBlazor CSS (باید قبل از site.css باشه تا override بشه) -->
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" /> <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
<!-- Custom styles (بعد از MudBlazor برای override) -->
<link href="css/site.css" rel="stylesheet" asp-append-version="true" />
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
<!-- d3-org-chart custom styles --> <!-- d3-org-chart custom styles -->
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" /> <link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
</head> </head>
@@ -42,6 +46,7 @@
<script src="js/d3-flextree.min.js"></script> <script src="js/d3-flextree.min.js"></script>
<script src="js/d3-org-chart3.js"></script> <script src="js/d3-org-chart3.js"></script>
<script src="js/org-chart.js" asp-append-version="true"></script> <script src="js/org-chart.js" asp-append-version="true"></script>
<script src="js/landing.js" asp-append-version="true"></script>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
<script> <script>
+101
View File
@@ -0,0 +1,101 @@
@inject ImageCacheService ImageCache
@if (_loading)
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle"
Width="@(ImgWidth > 0 ? $"{ImgWidth}px" : SkeletonWidth)"
Height="@(ImgHeight > 0 ? $"{ImgHeight}px" : SkeletonHeight)"
Class="@Class" Style="@Style" />
}
else if (!string.IsNullOrWhiteSpace(_resolvedSrc))
{
<MudImage Src="@_resolvedSrc"
Alt="@Alt"
ObjectFit="@ObjectFit"
ObjectPosition="@ObjectPosition"
Elevation="@Elevation"
Fluid="@Fluid"
Width="@ImgWidth"
Height="@ImgHeight"
Style="@Style"
Class="@Class" />
}
else if (!string.IsNullOrWhiteSpace(Fallback))
{
<div class="@($"d-flex align-center justify-center {Class}")" style="@Style">
<MudIcon Icon="@Fallback" Size="Size.Large" Style="color:rgba(0,0,0,.15);" />
</div>
}
@code {
/// <summary>مسیر تصویر (نسبی، data-URI، یا http)</summary>
[Parameter] public string? Path { get; set; }
/// <summary>متن جایگزین</summary>
[Parameter] public string? Alt { get; set; }
/// <summary>نحوه جایگیری تصویر</summary>
[Parameter] public ObjectFit ObjectFit { get; set; } = ObjectFit.Cover;
/// <summary>موقعیت تصویر</summary>
[Parameter] public ObjectPosition ObjectPosition { get; set; } = ObjectPosition.Center;
/// <summary>سایه تصویر</summary>
[Parameter] public int Elevation { get; set; }
/// <summary>تصویر سیال (100% عرض)</summary>
[Parameter] public bool Fluid { get; set; }
/// <summary>استایل inline</summary>
[Parameter] public string? Style { get; set; }
/// <summary>کلاس CSS</summary>
[Parameter] public string? Class { get; set; }
/// <summary>عرض تصویر (پیکسل) — مانند MudImage</summary>
[Parameter] public int? ImgWidth { get; set; }
/// <summary>ارتفاع تصویر (پیکسل) — مانند MudImage</summary>
[Parameter] public int? ImgHeight { get; set; }
/// <summary>عرض placeholder skeleton (مقدار CSS)</summary>
[Parameter] public string? SkeletonWidth { get; set; }
/// <summary>ارتفاع placeholder skeleton (مقدار CSS)</summary>
[Parameter] public string? SkeletonHeight { get; set; }
/// <summary>آیکون fallback هنگام عدم وجود تصویر</summary>
[Parameter] public string? Fallback { get; set; }
private string? _resolvedSrc;
private bool _loading;
private string? _lastPath;
protected override async Task OnParametersSetAsync()
{
// فقط وقتی Path تغییر کرده، resolve مجدد انجام شود
if (Path == _lastPath) return;
_lastPath = Path;
if (string.IsNullOrWhiteSpace(Path))
{
_resolvedSrc = null;
_loading = false;
return;
}
// اگر قبلاً base64 هست → نمایش فوری
if (Path.StartsWith("data:", StringComparison.Ordinal))
{
_resolvedSrc = Path;
_loading = false;
return;
}
_loading = true;
StateHasChanged();
_resolvedSrc = await ImageCache.ResolveAsync(Path);
_loading = false;
}
}
+91 -158
View File
@@ -1,168 +1,101 @@
@if (InlineMode) @if (InlineMode)
{ {
@* Inline rendering without MudDialog wrapper *@ @* Inline rendering without MudDialog wrapper *@
<MudStack Spacing="2"> <div class="auth-dialog-content">
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText> <MudStack Spacing="3" AlignItems="AlignItems.Center" Class="mb-4">
@PhoneOrVerifyContent() <MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled">
</MudStack> <MudIcon Icon="@(_currentStep == AuthStep.Phone ? Icons.Material.Outlined.PhoneAndroid : Icons.Material.Outlined.LockOpen)" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h5">@GetDialogTitle()</MudText>
</MudStack>
<PhoneVerifyForm @ref="_phoneVerifyForm"
CurrentStep="_currentStep"
PhoneRequest="_phoneRequest"
VerifyRequest="_verifyRequest"
CaptchaCode="@_captchaCode"
CaptchaInput="@_captchaInput"
CaptchaInputChanged="@(v => _captchaInput = v)"
OnRefreshCaptcha="GenerateCaptcha"
IsBusy="_isBusy"
ErrorMessage="@_errorMessage"
InfoMessage="@_infoMessage"
PhoneNumber="@_phoneNumber"
ResendRemaining="_resendRemaining"
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" />
</div>
} }
else else
{ {
<MudDialog> <MudDialog Class="auth-dialog-wrapper">
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
</TitleContent>
<DialogContent> <DialogContent>
@PhoneOrVerifyContent() <div class="auth-dialog-content">
<MudStack Spacing="2" AlignItems="AlignItems.Center" Class="mb-4 pt-2">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@(_currentStep == AuthStep.Phone ? Icons.Material.Outlined.PhoneAndroid : Icons.Material.Outlined.LockOpen)" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
</MudStack>
<PhoneVerifyForm @ref="_phoneVerifyForm"
CurrentStep="_currentStep"
PhoneRequest="_phoneRequest"
VerifyRequest="_verifyRequest"
CaptchaCode="@_captchaCode"
CaptchaInput="@_captchaInput"
CaptchaInputChanged="@(v => _captchaInput = v)"
OnRefreshCaptcha="GenerateCaptcha"
IsBusy="_isBusy"
ErrorMessage="@_errorMessage"
InfoMessage="@_infoMessage"
PhoneNumber="@_phoneNumber"
ResendRemaining="_resendRemaining"
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" />
<MudStack Class="mt-4" Spacing="2">
@if (_currentStep == AuthStep.Phone)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SendOtpAsync"
Disabled="_isBusy"
Class="rounded-lg"
FullWidth="true"
Size="Size.Large">
@if (_isBusy)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
ارسال رمز پویا
</MudButton>
}
else if (_currentStep == AuthStep.Verify)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="VerifyOtpAsync"
Disabled="_isBusy || IsVerificationLocked"
Class="rounded-lg"
FullWidth="true"
Size="Size.Large">
@if (_isBusy)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
تأیید و ورود
</MudButton>
}
@if (!HideCancelButton)
{
<MudButton Variant="Variant.Text"
OnClick="Cancel"
Disabled="_isBusy"
FullWidth="true">لغو
</MudButton>
}
</MudStack>
</div>
</DialogContent> </DialogContent>
<DialogActions>
@if (!HideCancelButton)
{
<MudButton Variant="Variant.Text"
OnClick="Cancel"
Disabled="_isBusy">لغو
</MudButton>
}
@if (_currentStep == AuthStep.Phone)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SendOtpAsync"
Disabled="_isBusy"
FullWidth="true">
ارسال رمز پویا
</MudButton>
}
else if (_currentStep == AuthStep.Verify)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="VerifyOtpAsync"
Disabled="_isBusy || IsVerificationLocked"
FullWidth="true">
تأیید و ورود
</MudButton>
}
</DialogActions>
</MudDialog> </MudDialog>
} }
@code {
private RenderFragment PhoneOrVerifyContent() => __builder =>
{
if (_currentStep == AuthStep.Phone)
{
// Phone Step
__builder.OpenComponent(0, typeof(MudText));
__builder.AddAttribute(1, "Typo", Typo.body2);
__builder.AddAttribute(2, "Class", "mb-4");
__builder.AddAttribute(3, "Align", Align.Center);
__builder.AddContent(4, "لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.");
__builder.CloseComponent();
<MudForm @ref="_phoneForm" Model="_phoneRequest">
<MudTextField @bind-Value="_phoneRequest.Mobile"
For="@(() => _phoneRequest.Mobile)"
Label="شماره موبایل"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن شماره موبایل الزامی است."
HelperText="مثال: 09121234567"
Class="mb-2"/>
@* @if (EnableCaptcha) *@
@* { *@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mt-2 mb-2">
<MudItem xs="7" md="12">
<MudTextField xs="4" Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید."/>
</MudItem>
<MudItem xs="4" >
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center"
Style="min-width:100px;min-height:48px;">
<MudText Typo="Typo.h5">@_captchaCode</MudText>
</MudPaper>
</MudItem>
<MudItem xs="1" >
<MudButton Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy"
OnClick="GenerateCaptcha">
</MudButton>
</MudItem>
</MudStack>
@* } *@
@* *@
<MudCheckBox T="bool" Required="true"
Label="شرایط و قوانین را می‌پذیرم"
RequiredError="برای ادامه باید شرایط و قوانین را بپذیرید."
Class="mb-1"/>
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2">@_errorMessage</MudAlert>
}
</MudForm>
}
else if (_currentStep == AuthStep.Verify)
{
// Verify Step
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را
وارد کنید.
</MudText>
<MudForm @ref="_verifyForm" Model="_verifyRequest">
<MudTextField @bind-Value="_verifyRequest.Code"
For="@(() => _verifyRequest.Code)"
Label="رمز پویا"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
HelperText="کد ۶ رقمی"
Class="mb-2"
MaxLength="6"/>
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2">@(_errorMessage)</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(_infoMessage))
{
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0"
Class="mb-2">@(_infoMessage)</MudAlert>
}
<MudStack Spacing="2">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_isBusy"
OnClick="ChangePhoneAsync">تغییر شماره
</MudButton>
</MudStack>
<MudDivider Class="my-2"/>
@if (_resendRemaining > 0)
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">امکان ارسال مجدد
تا @_resendRemaining ثانیه دیگر
</MudText>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">
ارسال مجدد رمز پویا
</MudButton>
}
</MudForm>
}
};
}
@@ -25,10 +25,9 @@ public partial class AuthDialog : IDisposable
public AuthStep _currentStep = AuthStep.Phone; public AuthStep _currentStep = AuthStep.Phone;
private readonly CreateNewOtpTokenRequest _phoneRequest = new(); private readonly CreateNewOtpTokenRequest _phoneRequest = new();
private MudForm? _phoneForm; private PhoneVerifyForm? _phoneVerifyForm;
private readonly VerifyOtpTokenRequest _verifyRequest = new(); private readonly VerifyOtpTokenRequest _verifyRequest = new();
private MudForm? _verifyForm;
private bool _isBusy; private bool _isBusy;
private string? _phoneNumber; private string? _phoneNumber;
@@ -79,11 +78,12 @@ public partial class AuthDialog : IDisposable
public async Task SendOtpAsync() public async Task SendOtpAsync()
{ {
_errorMessage = null; _errorMessage = null;
if (_phoneForm is null) var phoneForm = _phoneVerifyForm?.GetPhoneForm();
if (phoneForm is null)
return; return;
await _phoneForm.Validate(); await phoneForm.Validate();
if (!_phoneForm.IsValid) if (!phoneForm.IsValid)
return; return;
// if (EnableCaptcha) // if (EnableCaptcha)
@@ -154,11 +154,12 @@ public partial class AuthDialog : IDisposable
_errorMessage = null; _errorMessage = null;
_infoMessage = null; _infoMessage = null;
if (_verifyForm is null) var verifyForm = _phoneVerifyForm?.GetVerifyForm();
if (verifyForm is null)
return false; return false;
await _verifyForm.Validate(); await verifyForm.Validate();
if (!_verifyForm.IsValid) if (!verifyForm.IsValid)
return false; return false;
if (IsVerificationLocked) if (IsVerificationLocked)
@@ -236,6 +237,10 @@ public partial class AuthDialog : IDisposable
return true; return true;
} }
// اگه سرور RemainingAttempts برگردونده، از اون استفاده کن
if (response.RemainingAttempts > 0)
_attemptsLeft = response.RemainingAttempts;
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message); RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
} }
catch (RpcException rpcEx) catch (RpcException rpcEx)
@@ -135,7 +135,8 @@
new CMSMicroservice.Protobuf.Protos.OtpToken.CreateNewOtpTokenRequest new CMSMicroservice.Protobuf.Protos.OtpToken.CreateNewOtpTokenRequest
{ {
Mobile = await GetUserMobileAsync(), Mobile = await GetUserMobileAsync(),
Purpose = "ClubContract" Purpose = "signClubContract",
SignGuid = _signGuid.ToString()
}); });
if (response.Success) if (response.Success)
@@ -166,11 +167,9 @@
_isLoading = true; _isLoading = true;
try try
{ {
var userId = await GetCurrentUserIdAsync();
var response = await ClubMembershipClient.AcceptClubMembershipContractAsync( var response = await ClubMembershipClient.AcceptClubMembershipContractAsync(
new CMSMicroservice.Protobuf.Protos.ClubMembership.AcceptClubMembershipContractRequest new CMSMicroservice.Protobuf.Protos.ClubMembership.AcceptClubMembershipContractRequest
{ {
UserId = userId,
OtpCode = _otpCode, OtpCode = _otpCode,
SignGuid = _signGuid.ToString(), SignGuid = _signGuid.ToString(),
ContractHtml = GetClubContractHtml() ContractHtml = GetClubContractHtml()
@@ -0,0 +1,67 @@
@inject IJSRuntime JS
@* ═══════════════════════════════════════════════
EmptyState — Unified empty/not-found placeholder
Usage: <EmptyState Title="سفارشی ثبت نشده" />
<EmptyState Title="آدرسی یافت نشد"
Icon="@Icons.Material.Filled.LocationOff"
Description="ابتدا یک آدرس اضافه کنید."
ActionText="افزودن آدرس"
OnAction="OpenAddDialog" />
═══════════════════════════════════════════════ *@
<MudStack AlignItems="AlignItems.Center" Class="py-12" Spacing="3">
<MudIcon Icon="@Icon" Size="Size.Large" Color="Color.Default"
Class="mud-text-disabled" Style="font-size:3rem;" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">@Title</MudText>
@if (!string.IsNullOrWhiteSpace(Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary text-center"
Style="max-width:400px;">@Description</MudText>
}
@if (ChildContent is not null)
{
@ChildContent
}
else if (!string.IsNullOrWhiteSpace(ActionText))
{
@if (!string.IsNullOrWhiteSpace(ActionHref))
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="GoBack" Class="mt-2">@ActionText</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="OnAction" Class="mt-2">@ActionText</MudButton>
}
}
</MudStack>
@code {
/// <summary>Material icon string.</summary>
[Parameter] public string Icon { get; set; } = Icons.Material.Filled.Inbox;
/// <summary>Main heading text.</summary>
[Parameter] public string Title { get; set; } = "موردی یافت نشد";
/// <summary>Optional secondary description.</summary>
[Parameter] public string? Description { get; set; }
/// <summary>Optional CTA button text.</summary>
[Parameter] public string? ActionText { get; set; }
/// <summary>If set, CTA becomes a link instead of callback.</summary>
[Parameter] public string? ActionHref { get; set; }
/// <summary>CTA button callback.</summary>
[Parameter] public EventCallback OnAction { get; set; }
/// <summary>Optional custom content (replaces ActionText button).</summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
private async Task GoBack()
{
await JS.InvokeVoidAsync("history.back");
}
}
@@ -0,0 +1,18 @@
@* ═══════════════════════════════════════════════
LoadingState — Unified loading indicator
Usage: <LoadingState />
<LoadingState Message="در حال دریافت سفارشات..." />
═══════════════════════════════════════════════ *@
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
@if (!string.IsNullOrWhiteSpace(Message))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">@Message</MudText>
}
</MudStack>
@code {
/// <summary>Optional loading message displayed below spinner.</summary>
[Parameter] public string Message { get; set; } = "در حال بارگذاری...";
}
+180 -38
View File
@@ -9,10 +9,10 @@
<MudSnackbarProvider/> <MudSnackbarProvider/>
<MudLayout> <MudLayout>
<!-- AppBar --> <!-- AppBar -->
<MudAppBar Elevation="0" Color="Color.Transparent" Dense="true" Class="py-2" id="top"> <MudAppBar Elevation="1" Color="Color.Surface" Dense="true" Class="py-2" id="top">
<MudContainer MaxWidth="MaxWidth.Large" Class="d-flex align-center justify-space-between w-100"> <MudContainer MaxWidth="MaxWidth.Large" Class="d-flex align-center justify-space-between w-100">
<MudHidden Breakpoint="Breakpoint.SmAndUp"> <MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudIconButton Class="d-inline" <MudIconButton Class="d-inline"
Icon="@Icons.Material.Filled.Menu" Icon="@Icons.Material.Filled.Menu"
OnClick="@ToggleDrawer"/> OnClick="@ToggleDrawer"/>
@@ -21,7 +21,7 @@
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None"> <MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudHidden Breakpoint="Breakpoint.SmAndUp" Invert="true"> <MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudImage ObjectFit="ObjectFit.Cover" <MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" ObjectPosition="ObjectPosition.Center"
Width="32" Width="32"
@@ -32,37 +32,66 @@
<MudText Typo="Typo.h6">کارا بازار سلامت</MudText> <MudText Typo="Typo.h6">کارا بازار سلامت</MudText>
</MudStack> </MudStack>
</MudLink> </MudLink>
@* ── Store context badge (desktop) ── *@
@if (IsInRegularStore)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small"
Class="d-none d-md-flex" Icon="@Icons.Material.Filled.Storefront">فروشگاه</MudChip>
}
else if (IsInDiscountStore)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Class="d-none d-md-flex" Icon="@Icons.Material.Filled.Loyalty">فروشگاه تخفیفی</MudChip>
}
</div> </div>
<div class="d-none d-md-flex align-center gap-10"> <div class="d-none d-md-flex align-center gap-10">
@if (_isAuthenticated) @if (IsInRegularStore && _isAuthenticated)
{ {
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link"> @* ── Regular store nav ── *@
محصولات <MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات</MudLink>
</MudLink> <MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید <MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات
من
</MudLink>
} }
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" Class="mud-link">سوالات متداول else if (IsInDiscountStore && _isAuthenticated)
</MudLink> {
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" Class="mud-link">ارتباط با @* ── Discount store nav ── *@
ما <MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات تخفیفی</MudLink>
</MudLink> <MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" Class="mud-link">درباره ما <MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
</MudLink> }
else
{
@* ── Default global nav (not inside any store) ── *@
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1" Class="mud-link">فروشگاه‌ها</MudLink>
}
}
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" Class="mud-link">سوالات متداول</MudLink>
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" Class="mud-link">بلاگ</MudLink>
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" Class="mud-link">ارتباط با ما</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" Class="mud-link">درباره ما</MudLink>
</div> </div>
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
@if (_isAuthenticated) @if (_isAuthenticated)
{ {
@* <MudBadge Content="@_cartCount" Color="Color.Error" Overlap="true" Visible="@(_cartCount > 0)"> *@ @* ── Cart icon: only visible when inside a store ── *@
@* *@ @if (IsInRegularStore)
@* </MudBadge> *@ {
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart" Color="@(_cartCount > 0?Color.Success:Color.Inherit)" <MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
Href="@(RouteConstants.Store.Cart)" /> Color="@(_cartCount > 0 ? Color.Success : Color.Inherit)"
Href="@(RouteConstants.Store.Cart)" />
}
else if (IsInDiscountStore)
{
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
Color="@(_discountCartCount > 0 ? Color.Error : Color.Inherit)"
Href="@(RouteConstants.DiscountStore.Cart)" />
}
@* دکمه بروزرسانی *@ @* دکمه بروزرسانی *@
@if (_hasUpdate) @if (_hasUpdate)
{ {
@@ -74,7 +103,10 @@
</MudBadge> </MudBadge>
} }
<MudMenu Icon="@Icons.Material.Filled.Person" Color="Color.Inherit" Size="Size.Medium"> <MudMenu Icon="@Icons.Material.Filled.Person" Color="Color.Inherit" Size="Size.Medium">
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(!AuthService.IsCompleteRegister())"> <MudMenuItem OnClick="NavigateToProfile" Disabled="@(!_isCompleteRegister)" Icon="@Icons.Material.Outlined.Dashboard">
داشبورد
</MudMenuItem>
<MudMenuItem OnClick="NavigateToHub" Disabled="@(!_isCompleteRegister)" Icon="@Icons.Material.Outlined.Person">
پروفایل پروفایل
</MudMenuItem> </MudMenuItem>
<MudDivider/> <MudDivider/>
@@ -110,19 +142,49 @@
<MudStack Spacing="2"> <MudStack Spacing="2">
@if (_isAuthenticated) @if (_isAuthenticated)
{ {
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" @if (IsInRegularStore)
OnClick="() => _drawerOpen=false">محصولات {
</MudLink> <MudText Typo="Typo.overline" Class="mud-text-secondary">فروشگاه</MudText>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" <MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سبد خرید OnClick="() => _drawerOpen=false">محصولات</MudLink>
</MudLink> <MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1"
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
OnClick="() => _drawerOpen=false">سفارشات من <MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1"
</MudLink> OnClick="() => _drawerOpen=false">سفارشات من</MudLink>
<MudDivider Class="my-1" />
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false" Class="mud-text-secondary">
← بازگشت به فروشگاه‌ها
</MudLink>
}
else if (IsInDiscountStore)
{
<MudText Typo="Typo.overline" Style="color:var(--mud-palette-error);">فروشگاه تخفیفی</MudText>
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">محصولات تخفیفی</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سفارشات من</MudLink>
<MudDivider Class="my-1" />
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false" Class="mud-text-secondary">
← بازگشت به فروشگاه‌ها
</MudLink>
}
else
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">فروشگاه‌ها</MudLink>
}
<MudDivider Class="my-1" />
} }
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false"> <MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
سوالات متداول سوالات متداول
</MudLink> </MudLink>
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
بلاگ
</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false"> <MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
درباره ما درباره ما
</MudLink> </MudLink>
@@ -165,13 +227,93 @@
</MudStack> </MudStack>
</MudDrawer> </MudDrawer>
<MudMainContent> <MudMainContent Class="main-content-wrapper">
@Body @Body
@if (DeviceDetector.IsDesktop()) <MudHidden Breakpoint="Breakpoint.SmAndDown">
{
<Footer/> <Footer/>
} </MudHidden>
</MudMainContent> </MudMainContent>
<!-- Mobile Bottom Navigation -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudPaper Class="bottom-nav" Elevation="8">
<MudStack Row="true" Justify="Justify.SpaceAround" AlignItems="AlignItems.Center" Class="py-1">
@if (IsInRegularStore && _isAuthenticated)
{
@* ── Regular store bottom nav ── *@
<MudLink Href="@(RouteConstants.Store.Products)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Storefront" Size="Size.Medium" />
<MudText Typo="Typo.caption">محصولات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.ShoppingCart" Size="Size.Medium" Color="@(_cartCount > 0 ? Color.Success : Color.Default)" />
<MudText Typo="Typo.caption">سبد خرید</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Receipt" Size="Size.Medium" />
<MudText Typo="Typo.caption">سفارشات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">داشبورد</MudText>
</MudLink>
}
else if (IsInDiscountStore && _isAuthenticated)
{
@* ── Discount store bottom nav ── *@
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Loyalty" Size="Size.Medium" Color="Color.Error" />
<MudText Typo="Typo.caption">محصولات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.ShoppingCart" Size="Size.Medium" Color="@(_discountCartCount > 0 ? Color.Error : Color.Default)" />
<MudText Typo="Typo.caption">سبد خرید</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Receipt" Size="Size.Medium" />
<MudText Typo="Typo.caption">سفارشات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">داشبورد</MudText>
</MudLink>
}
else
{
@* ── Default bottom nav (outside stores) ── *@
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">خانه</MudText>
</MudLink>
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Store" Size="Size.Medium" />
<MudText Typo="Typo.caption">فروشگاه‌ها</MudText>
</MudLink>
}
<MudLink Href="@(RouteConstants.Blog.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Article" Size="Size.Medium" />
<MudText Typo="Typo.caption">بلاگ</MudText>
</MudLink>
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Profile.Hub)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Person" Size="Size.Medium" />
<MudText Typo="Typo.caption">پروفایل</MudText>
</MudLink>
}
else
{
<MudLink Class="bottom-nav-item" @onclick="OpenAuthDialog">
<MudIcon Icon="@Icons.Material.Outlined.Login" Size="Size.Medium" />
<MudText Typo="Typo.caption">ورود</MudText>
</MudLink>
}
}
</MudStack>
</MudPaper>
</MudHidden>
</MudLayout> </MudLayout>
</MudRTLProvider> </MudRTLProvider>
+116 -1
View File
@@ -1,6 +1,7 @@
using Blazored.LocalStorage; using Blazored.LocalStorage;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
@@ -16,6 +17,15 @@ public partial class MainLayout : IDisposable
private bool _isAuthenticated; private bool _isAuthenticated;
private string? _email; private string? _email;
private int _cartCount; private int _cartCount;
private int _discountCartCount;
private bool _isCompleteRegister;
// ── Store Context ──
private enum StoreContext { None, Regular, Discount }
private StoreContext _storeCtx = StoreContext.None;
private bool IsInRegularStore => _storeCtx == StoreContext.Regular;
private bool IsInDiscountStore => _storeCtx == StoreContext.Discount;
private bool IsInAnyStore => _storeCtx != StoreContext.None;
// متغیرهای بروزرسانی // متغیرهای بروزرسانی
private bool _hasUpdate; private bool _hasUpdate;
@@ -26,6 +36,7 @@ public partial class MainLayout : IDisposable
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!; [Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private CartService CartService { get; set; } = default!; [Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private AppVersionService AppVersionService { get; set; } = default!; [Inject] private AppVersionService AppVersionService { get; set; } = default!;
private void ToggleTheme() => _isDark = !_isDark; private void ToggleTheme() => _isDark = !_isDark;
@@ -35,20 +46,34 @@ public partial class MainLayout : IDisposable
await JSRuntime.InvokeVoidAsync("history.back"); await JSRuntime.InvokeVoidAsync("history.back");
} }
protected override void OnInitialized()
{
Navigation.LocationChanged += OnLocationChanged;
UpdateStoreContext();
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
await JSRuntime.InvokeVoidAsync("changeNavBgOnBodyScroll", "top", null, 1); await JSRuntime.InvokeVoidAsync("changeNavBgOnBodyScroll", "top", null, 1);
await CheckAuthStatus(); await CheckAuthStatus();
await EnforceAuthGuardAsync();
if (_isAuthenticated) if (_isAuthenticated)
{ {
// بررسی وضعیت ثبت‌نام کامل
_isCompleteRegister = await AuthService.IsCompleteRegisterAsync();
// لود سبد خرید فقط برای کاربر لاگین شده // لود سبد خرید فقط برای کاربر لاگین شده
await CartService.EnsureInitializedAsync(); await CartService.EnsureInitializedAsync();
CartService.OnChange += OnCartChanged; CartService.OnChange += OnCartChanged;
_cartCount = CartService.Count; _cartCount = CartService.Count;
await DiscountCartService.EnsureInitializedAsync();
DiscountCartService.OnChange += OnDiscountCartChanged;
_discountCartCount = DiscountCartService.Count;
// چک کردن بروزرسانی (بدون نمایش popup - فقط برای نشون دادن آیکون) // چک کردن بروزرسانی (بدون نمایش popup - فقط برای نشون دادن آیکون)
await CheckForUpdateSilentlyAsync(); await CheckForUpdateSilentlyAsync();
} }
@@ -57,6 +82,83 @@ public partial class MainLayout : IDisposable
} }
} }
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
UpdateStoreContext();
_ = InvokeAsync(async () =>
{
await EnforceAuthGuardAsync();
StateHasChanged();
});
}
/// <summary>
/// If the current route requires authentication and the user is not logged in, redirect to home.
/// </summary>
private async Task EnforceAuthGuardAsync()
{
if (_isAuthenticated) return;
// Re-check in case token was set after initial render
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
if (_isAuthenticated) return;
var path = new Uri(Navigation.Uri).AbsolutePath.ToLowerInvariant();
if (IsProtectedRoute(path))
{
Navigation.NavigateTo(RouteConstants.Main.MainPage, replace: true);
}
}
private static bool IsProtectedRoute(string path)
{
// Public routes — no auth needed
if (path is "/" or "/register" or "/about" or "/faq" or "/contact"
or "/packages" or "/stores" or "/products" or "/categories")
return false;
if (path.StartsWith("/blog")) return false;
if (path.StartsWith("/package/")) return false;
if (path.StartsWith("/product/")) return false;
if (path == "/discount-store" || path.StartsWith("/discount-store/product/")) return false;
// Everything else is protected
if (path.StartsWith("/profile")) return true;
if (path.StartsWith("/commission")) return true;
if (path.StartsWith("/network")) return true;
if (path.StartsWith("/club")) return true;
if (path.StartsWith("/checkout")) return true;
if (path.StartsWith("/cart")) return true;
if (path.StartsWith("/orders") || path.StartsWith("/order/") || path.StartsWith("/order-tracking/")) return true;
if (path.StartsWith("/my-packages") || path.StartsWith("/my-orders") || path.StartsWith("/my-cart")) return true;
if (path.StartsWith("/discount-store/cart") || path.StartsWith("/discount-store/checkout")
|| path.StartsWith("/discount-store/orders") || path.StartsWith("/discount-store/order/")) return true;
return false;
}
private void UpdateStoreContext()
{
var path = new Uri(Navigation.Uri).AbsolutePath.ToLowerInvariant();
// Discount store routes all start with /discount-store
if (path.StartsWith("/discount-store"))
{
_storeCtx = StoreContext.Discount;
}
// Regular store routes
else if (path.StartsWith("/products") || path.StartsWith("/product/") ||
path.StartsWith("/cart") || path.StartsWith("/checkout-summary") ||
path.StartsWith("/orders") || path.StartsWith("/order/") ||
path.StartsWith("/order-tracking/") || path.StartsWith("/categories"))
{
_storeCtx = StoreContext.Regular;
}
else
{
_storeCtx = StoreContext.None;
}
}
/// <summary> /// <summary>
/// چک کردن بروزرسانی بدون نمایش popup /// چک کردن بروزرسانی بدون نمایش popup
/// </summary> /// </summary>
@@ -131,6 +233,12 @@ public partial class MainLayout : IDisposable
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
} }
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
private async Task CheckAuthStatus() private async Task CheckAuthStatus()
{ {
_isAuthenticated = await AuthService.IsAuthenticatedAsync(); _isAuthenticated = await AuthService.IsAuthenticatedAsync();
@@ -154,6 +262,11 @@ public partial class MainLayout : IDisposable
Navigation.NavigateTo(RouteConstants.Profile.Index); Navigation.NavigateTo(RouteConstants.Profile.Index);
} }
private void NavigateToHub()
{
Navigation.NavigateTo(RouteConstants.Profile.Hub);
}
private async Task Logout() private async Task Logout()
{ {
await AuthService.LogoutAsync(); await AuthService.LogoutAsync();
@@ -165,5 +278,7 @@ public partial class MainLayout : IDisposable
public void Dispose() public void Dispose()
{ {
CartService.OnChange -= OnCartChanged; CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
Navigation.LocationChanged -= OnLocationChanged;
} }
} }
@@ -0,0 +1,40 @@
@inject NavigationManager Navigation
@inject IJSRuntime JS
<div class="page-header">
<MudStack Spacing="0">
<MudText Typo="Typo.h5">@Title</MudText>
@if (!string.IsNullOrWhiteSpace(Subtitle))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">@Subtitle</MudText>
}
</MudStack>
@if (ShowBack)
{
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="GoBack">بازگشت</MudButton>
}
</div>
@code {
[Parameter] public string Title { get; set; } = string.Empty;
/// <summary>
/// Optional subtitle text displayed below the title.
/// </summary>
[Parameter] public string? Subtitle { get; set; }
/// <summary>
/// Fallback URL if there's no browser history. Uses history.back() first.
/// </summary>
[Parameter] public string? BackHref { get; set; }
/// <summary>
/// Whether to show the back button. Default is true.
/// </summary>
[Parameter] public bool ShowBack { get; set; } = true;
private async Task GoBack()
{
await JS.InvokeVoidAsync("history.back");
}
}
@@ -0,0 +1,96 @@
@using static FrontOffice.Main.Shared.AuthDialog
@if (CurrentStep == AuthStep.Phone)
{
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary" Align="Align.Center">
لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.
</MudText>
<MudForm @ref="_phoneForm" Model="PhoneRequest">
<MudTextField @bind-Value="PhoneRequest.Mobile"
Label="شماره موبایل"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
Placeholder="شماره موبایل خود را وارد کنید"
RequiredError="وارد کردن شماره موبایل الزامی است."
Class="mb-3" />
<div class="captcha-row mb-3">
<MudTextField Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" Value="CaptchaInput"
ValueChanged="@((string v) => CaptchaInputChanged.InvokeAsync(v))"
Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید." />
<MudPaper Elevation="0" Class="captcha-box d-flex align-center justify-center">
<MudText Typo="Typo.h5">@CaptchaCode</MudText>
</MudPaper>
<MudIconButton Icon="@Icons.Material.Outlined.Refresh" Color="Color.Primary"
Disabled="IsBusy" OnClick="OnRefreshCaptcha" Size="Size.Medium" />
</div>
<MudCheckBox T="bool" Required="true"
Label="شرایط و قوانین را می‌پذیرم"
RequiredError="برای ادامه باید شرایط و قوانین را بپذیرید."
Color="Color.Primary"
Class="mb-2" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@ErrorMessage</MudAlert>
}
</MudForm>
}
else if (CurrentStep == AuthStep.Verify)
{
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary" Align="Align.Center">
رمز پویا شش رقمی ارسال شده به <strong>@PhoneNumber</strong> را وارد کنید.
</MudText>
<MudForm @ref="_verifyForm" Model="VerifyRequest">
<MudTextField @bind-Value="VerifyRequest.Code"
Label="رمز پویا"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
Class="mb-3"
MaxLength="6" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@ErrorMessage</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(InfoMessage))
{
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@InfoMessage</MudAlert>
}
<MudDivider Class="my-3" />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="IsBusy"
OnClick="OnChangePhone" Size="Size.Small">
<MudIcon Icon="@Icons.Material.Outlined.Edit" Size="Size.Small" Class="me-1" /> تغییر شماره
</MudButton>
@if (ResendRemaining > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
ارسال مجدد تا @ResendRemaining ثانیه
</MudText>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="IsBusy"
OnClick="OnResendOtp" Size="Size.Small">
ارسال مجدد
</MudButton>
}
</MudStack>
</MudForm>
}
@@ -0,0 +1,42 @@
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using static FrontOffice.Main.Shared.AuthDialog;
namespace FrontOffice.Main.Shared;
public partial class PhoneVerifyForm
{
// ── Step state ──
[Parameter, EditorRequired] public AuthStep CurrentStep { get; set; }
// ── Phone step ──
[Parameter, EditorRequired] public CreateNewOtpTokenRequest PhoneRequest { get; set; } = default!;
// ── Verify step ──
[Parameter, EditorRequired] public VerifyOtpTokenRequest VerifyRequest { get; set; } = default!;
// ── Captcha ──
[Parameter] public string? CaptchaCode { get; set; }
[Parameter] public string? CaptchaInput { get; set; }
[Parameter] public EventCallback<string?> CaptchaInputChanged { get; set; }
[Parameter] public EventCallback OnRefreshCaptcha { get; set; }
// ── Shared state ──
[Parameter] public bool IsBusy { get; set; }
[Parameter] public string? ErrorMessage { get; set; }
[Parameter] public string? InfoMessage { get; set; }
[Parameter] public string? PhoneNumber { get; set; }
[Parameter] public int ResendRemaining { get; set; }
// ── Verify actions ──
[Parameter] public EventCallback OnChangePhone { get; set; }
[Parameter] public EventCallback OnResendOtp { get; set; }
// ── Internal form refs (exposed via public methods) ──
private MudForm? _phoneForm;
private MudForm? _verifyForm;
public MudForm? GetPhoneForm() => _phoneForm;
public MudForm? GetVerifyForm() => _verifyForm;
}
@@ -6,7 +6,12 @@ public class AuthDialogService
{ {
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IDeviceDetector _deviceDetector; private readonly IDeviceDetector _deviceDetector;
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; private readonly DialogOptions _dialogOptions = new()
{
MaxWidth = MaxWidth.ExtraSmall,
FullWidth = true,
CloseButton = true
};
public AuthDialogService(IDialogService dialogService, IDeviceDetector deviceDetector) public AuthDialogService(IDialogService dialogService, IDeviceDetector deviceDetector)
{ {
@@ -18,12 +23,7 @@ public class AuthDialogService
public async Task ShowAuthDialogAsync() public async Task ShowAuthDialogAsync()
{ {
// Pick dialog size based on device type var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", _dialogOptions);
var options = _deviceDetector.IsMobile()
? new DialogOptions() { FullScreen = true}
: _normalWidth;
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", options);
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Canceled) if (!result.Canceled)
@@ -54,7 +54,7 @@ public class AuthService
} }
public bool IsCompleteRegister() public bool IsCompleteRegister()
{ {
InitUserAuthInfo().GetAwaiter(); InitUserAuthInfo().GetAwaiter().GetResult();
if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract) if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract)
{ {
return true; return true;
@@ -0,0 +1,60 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for fetching active blog categories (customer-facing).
/// </summary>
public class BlogCategoryService
{
private readonly CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient _client;
public BlogCategoryService(CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient client)
{
_client = client;
}
/// <summary>
/// Get all active blog categories (for blog sidebar / filter).
/// </summary>
public async Task<List<BlogCategoryDto>> GetActiveCategoriesAsync()
{
try
{
var response = await _client.GetActiveBlogCategoriesAsync(
new CMSMicroservice.Protobuf.Protos.BlogCategory.GetActiveBlogCategoriesRequest());
if (response?.Categories == null || response.Categories.Count == 0)
return new();
return response.Categories
.OrderBy(c => c.SortOrder)
.Select(c => new BlogCategoryDto
{
Id = c.Id,
Title = c.Title,
Slug = c.Slug,
Description = c.Description,
IconName = c.IconName,
PostCount = c.PostCount
})
.ToList();
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogCategoryService.GetActiveCategoriesAsync error: {ex.Message}");
#endif
return new();
}
}
}
// ── DTO ──
public class BlogCategoryDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Description { get; set; }
public string? IconName { get; set; }
public int PostCount { get; set; }
}
@@ -0,0 +1,204 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for fetching published blog posts (customer-facing).
/// Used by Landing Page (latest posts) and future Blog pages.
/// </summary>
public class BlogPostService
{
private readonly CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient _client;
public BlogPostService(CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient client)
{
_client = client;
}
/// <summary>
/// Get featured blog posts (for landing page hero / highlights).
/// </summary>
public async Task<List<BlogPostCardDto>> GetFeaturedPostsAsync(int count = 3)
{
try
{
var response = await _client.GetFeaturedBlogPostsAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.GetFeaturedBlogPostsRequest { Count = count });
return MapToCardDtos(response?.Models);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetFeaturedPostsAsync error: {ex.Message}");
#endif
return new();
}
}
/// <summary>
/// Get latest published posts with optional category filter.
/// </summary>
public async Task<BlogPostListResult> GetPublishedPostsAsync(int page = 1, int pageSize = 9, string? search = null, long? categoryId = null)
{
try
{
var request = new CMSMicroservice.Protobuf.Protos.BlogPost.GetPublishedBlogPostsRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = page,
PageSize = pageSize
}
};
if (!string.IsNullOrWhiteSpace(search))
request.SearchTerm = search;
if (categoryId.HasValue)
request.CategoryId = categoryId.Value;
var response = await _client.GetPublishedBlogPostsAsync(request);
return new BlogPostListResult
{
Posts = MapToCardDtos(response?.Models),
TotalCount = (int)(response?.MetaData?.TotalCount ?? 0),
TotalPages = (int)(response?.MetaData?.TotalPage ?? 0),
CurrentPage = page
};
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetPublishedPostsAsync error: {ex.Message}");
#endif
return new();
}
}
/// <summary>
/// Get a single blog post by slug (for blog detail page).
/// </summary>
public async Task<BlogPostDetailDto?> GetBySlugAsync(string slug)
{
try
{
var response = await _client.GetBlogPostBySlugAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.GetBlogPostBySlugRequest { Slug = slug });
if (response == null || response.Id <= 0) return null;
return new BlogPostDetailDto
{
Id = response.Id,
Title = response.Title,
Slug = response.Slug,
Summary = response.Summary,
HtmlContent = response.HtmlContent,
FeaturedImagePath = response.FeaturedImagePath,
FeaturedImageThumbnailPath = response.FeaturedImageThumbnailPath,
PublishedAt = response.PublishedAt?.ToDateTime(),
ViewCount = response.ViewCount,
IsFeatured = response.IsFeatured,
Categories = response.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new(),
Tags = response.Tags?.Select(t => new BlogTagInfo { Id = t.Id, Title = t.Title, Name = t.Name }).ToList() ?? new()
};
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetBySlugAsync error: {ex.Message}");
#endif
return null;
}
}
/// <summary>
/// Increment view count for a blog post.
/// </summary>
public async Task IncrementViewCountAsync(long postId)
{
try
{
await _client.IncrementViewCountAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.IncrementViewCountRequest { Id = postId });
}
catch
{
// Fire and forget — don't fail the page
}
}
// ── Helpers ──
private static List<BlogPostCardDto> MapToCardDtos(
Google.Protobuf.Collections.RepeatedField<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostListItem>? models)
{
if (models == null || models.Count == 0) return new();
return models.Select(m => new BlogPostCardDto
{
Id = m.Id,
Title = m.Title,
Slug = m.Slug,
Summary = m.Summary,
ThumbnailUrl = m.FeaturedImageThumbnailPath,
PublishedAt = m.PublishedAt?.ToDateTime(),
ViewCount = m.ViewCount,
IsFeatured = m.IsFeatured,
Categories = m.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new()
}).ToList();
}
}
// ── DTOs ──
public class BlogPostCardDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Summary { get; set; }
public string? ThumbnailUrl { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public bool IsFeatured { get; set; }
public List<BlogCategoryInfo> Categories { get; set; } = new();
}
public class BlogPostDetailDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Summary { get; set; }
public string HtmlContent { get; set; } = string.Empty;
public string? FeaturedImagePath { get; set; }
public string? FeaturedImageThumbnailPath { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public bool IsFeatured { get; set; }
public List<BlogCategoryInfo> Categories { get; set; } = new();
public List<BlogTagInfo> Tags { get; set; } = new();
}
public class BlogPostListResult
{
public List<BlogPostCardDto> Posts { get; set; } = new();
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public int CurrentPage { get; set; }
}
public class BlogCategoryInfo
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
}
public class BlogTagInfo
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
@@ -220,7 +220,7 @@ public class CartService
cartId:model.Id, cartId:model.Id,
ProductId: model.ProductId, ProductId: model.ProductId,
Title: model.ProductTitle ?? string.Empty, Title: model.ProductTitle ?? string.Empty,
ImageUrl: string.IsNullOrWhiteSpace(model.ProductThumbnailPath) ? string.Empty : UrlUtility.DownloadUrl + model.ProductThumbnailPath, ImageUrl: model.ProductThumbnailPath,
UnitPrice: model.ProductPrice, UnitPrice: model.ProductPrice,
Quantity: model.Count > 0 ? model.Count : 1) Quantity: model.Count > 0 ? model.Count : 1)
{ {
+110 -35
View File
@@ -4,87 +4,162 @@ namespace FrontOffice.Main.Utilities;
public static class CustomMudTheme public static class CustomMudTheme
{ {
// ── Shared font stack ──
private static readonly string[] VazirFontFamily = ["Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif"];
public static MudTheme CustomMudBlazorTheme { get; set; } = new() public static MudTheme CustomMudBlazorTheme { get; set; } = new()
{ {
// ═══════════════════════════════════════
// Light Palette
// ═══════════════════════════════════════
PaletteLight = new PaletteLight() PaletteLight = new PaletteLight()
{ {
Primary = "#0380C0", Primary = "#6366f1", // Indigo — هویت اصلی برند
Background = "#F5F5F5", PrimaryDarken = "#4f46e5",
AppbarBackground = "#F5F5F5", PrimaryLighten = "#818cf8",
TextPrimary = Colors.Gray.Darken3, PrimaryContrastText = "#FFFFFF",
Secondary = "#8b5cf6", // Purple
SecondaryDarken = "#7c3aed",
SecondaryLighten = "#a78bfa",
SecondaryContrastText = "#FFFFFF",
Tertiary = "#06b6d4", // Cyan — accent
Info = "#3b82f6",
InfoContrastText = "#FFFFFF",
Success = "#10b981",
SuccessContrastText = "#FFFFFF",
Warning = "#f59e0b",
WarningContrastText = "#FFFFFF",
Error = "#ef4444",
ErrorContrastText = "#FFFFFF",
Background = "#f8f9fc", // Slightly cooler than plain gray
Surface = "#FFFFFF", Surface = "#FFFFFF",
Divider = "#B2BFCB", AppbarBackground = "#f8f9fc",
AppbarText = "#424242",
TextPrimary = "#1e293b", // Slate 800
TextSecondary = "#64748b", // Slate 500
TextDisabled = "#94a3b8", // Slate 400
Divider = "#e2e8f0", // Slate 200 — softer than old #B2BFCB
DrawerBackground = "#FFFFFF",
DrawerText = "#1e293b",
DrawerIcon = "#64748b",
}, },
// ═══════════════════════════════════════
// Dark Palette
// ═══════════════════════════════════════
PaletteDark = new PaletteDark()
{
Primary = "#818cf8", // Lighter indigo for dark BG
PrimaryDarken = "#6366f1",
PrimaryLighten = "#a5b4fc",
PrimaryContrastText = "#0f0f23",
Secondary = "#a78bfa",
SecondaryDarken = "#8b5cf6",
SecondaryLighten = "#c4b5fd",
SecondaryContrastText = "#0f0f23",
Tertiary = "#22d3ee",
Info = "#60a5fa",
InfoContrastText = "#0f172a",
Success = "#34d399",
SuccessContrastText = "#0f172a",
Warning = "#fbbf24",
WarningContrastText = "#0f172a",
Error = "#f87171",
ErrorContrastText = "#0f172a",
Background = "#0f172a", // Slate 900
Surface = "#1e293b", // Slate 800
AppbarBackground = "#0f172a",
AppbarText = "#e2e8f0",
TextPrimary = "#f1f5f9", // Slate 100
TextSecondary = "#94a3b8", // Slate 400
TextDisabled = "#475569", // Slate 600
Divider = "#334155", // Slate 700
DrawerBackground = "#1e293b",
DrawerText = "#f1f5f9",
DrawerIcon = "#94a3b8",
},
// ═══════════════════════════════════════
// Layout Properties
// ═══════════════════════════════════════
LayoutProperties = new LayoutProperties
{
DefaultBorderRadius = "12px",
AppbarHeight = "56px",
DrawerWidthLeft = "280px",
DrawerWidthRight = "280px",
},
// ═══════════════════════════════════════
// Typography
// ═══════════════════════════════════════
Typography = new Typography Typography = new Typography
{ {
Default = new DefaultTypography() Default = new DefaultTypography() { FontFamily = VazirFontFamily },
{
FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" }
},
H1 = new H1Typography() H1 = new H1Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = "1.70", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "2rem", LineHeight = "1.70",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
H2 = new H2Typography() H2 = new H2Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = "1.65", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "1.875rem", LineHeight = "1.65",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
H3 = new H3Typography() H3 = new H3Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = "1.60", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "1.5rem", LineHeight = "1.60",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
H4 = new H4Typography() H4 = new H4Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = "1.55", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "1.25rem", LineHeight = "1.55",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
H5 = new H5Typography() H5 = new H5Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = "1.50", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "1.125rem", LineHeight = "1.50",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
H6 = new H6Typography() H6 = new H6Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.45", FontWeight = "800", FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.45",
LetterSpacing = "normal" FontWeight = "800", LetterSpacing = "normal"
}, },
Subtitle1 = new Subtitle1Typography() Subtitle1 = new Subtitle1Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.62", FontWeight = "500", FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.62",
LetterSpacing = "normal" FontWeight = "500", LetterSpacing = "normal"
}, },
Subtitle2 = new Subtitle2Typography() Subtitle2 = new Subtitle2Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "500", FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.60",
LetterSpacing = "normal" FontWeight = "500", LetterSpacing = "normal"
}, },
Body1 = new Body1Typography() Body1 = new Body1Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.85", FontWeight = "400", FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.85",
LetterSpacing = "normal" FontWeight = "400", LetterSpacing = "normal"
}, },
Body2 = new Body2Typography() Body2 = new Body2Typography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.80", FontWeight = "400", FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.80",
LetterSpacing = "normal" FontWeight = "400", LetterSpacing = "normal"
}, },
Caption = new CaptionTypography() Caption = new CaptionTypography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "400", FontFamily = VazirFontFamily, FontSize = "0.75rem", LineHeight = "1.60",
LetterSpacing = "normal" FontWeight = "400", LetterSpacing = "normal"
}, },
Overline = new OverlineTypography() Overline = new OverlineTypography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "500", FontFamily = VazirFontFamily, FontSize = "0.75rem", LineHeight = "1.60",
LetterSpacing = "normal" FontWeight = "500", LetterSpacing = "normal"
}, },
Button = new ButtonTypography() Button = new ButtonTypography()
{ {
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "600", FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.60",
LetterSpacing = "normal", TextTransform = "none" FontWeight = "600", LetterSpacing = "normal", TextTransform = "none"
} }
} }
}; };
@@ -0,0 +1,151 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using Blazored.LocalStorage;
namespace FrontOffice.Main.Utilities;
public record DiscountCartItem(
long ProductId,
string Title,
string ImageUrl,
long UnitPrice,
int MaxDiscountPercent,
int Quantity,
long TotalPrice,
long DiscountAmount,
long FinalPrice,
int RemainingCount);
public class DiscountCartService
{
private readonly DiscountShoppingCartContract.DiscountShoppingCartContractClient _client;
private readonly ILocalStorageService _localStorage;
private readonly List<DiscountCartItem> _items = new();
private const string TokenStorageKey = "auth:token";
private bool _isInitialized;
public event Action? OnChange;
public DiscountCartService(
DiscountShoppingCartContract.DiscountShoppingCartContractClient client,
ILocalStorageService localStorage)
{
_client = client;
_localStorage = localStorage;
}
public IReadOnlyList<DiscountCartItem> Items => _items.AsReadOnly();
public long TotalPrice => _items.Sum(i => i.TotalPrice);
public long TotalDiscount => _items.Sum(i => i.DiscountAmount);
public long FinalPrice => _items.Sum(i => i.FinalPrice);
public int Count => _items.Sum(i => i.Quantity);
public async Task AddAsync(long productId, int count = 1)
{
if (!await IsAuthenticatedAsync()) return;
try
{
await _client.AddToCartAsync(new AddToCartRequest
{
ProductId = productId,
Count = count
});
await LoadFromServerAsync();
}
catch { /* best-effort */ }
}
public async Task UpdateQuantityAsync(long productId, int newCount)
{
if (!await IsAuthenticatedAsync()) return;
if (newCount <= 0)
{
await RemoveAsync(productId);
return;
}
try
{
await _client.UpdateCartItemCountAsync(new UpdateCartItemCountRequest
{
ProductId = productId,
NewCount = newCount
});
await LoadFromServerAsync();
}
catch { /* best-effort */ }
}
public async Task RemoveAsync(long productId)
{
if (!await IsAuthenticatedAsync()) return;
try
{
await _client.RemoveFromCartAsync(new RemoveFromCartRequest
{
ProductId = productId
});
_items.RemoveAll(i => i.ProductId == productId);
Notify();
}
catch { /* best-effort */ }
}
public async Task ClearAsync()
{
if (!await IsAuthenticatedAsync()) return;
try
{
await _client.ClearCartAsync(new ClearCartRequest());
_items.Clear();
Notify();
}
catch { /* best-effort */ }
}
public async Task EnsureInitializedAsync()
{
if (_isInitialized) return;
if (await IsAuthenticatedAsync())
{
await LoadFromServerAsync();
}
_isInitialized = true;
}
private async Task LoadFromServerAsync()
{
if (!await IsAuthenticatedAsync()) return;
try
{
var response = await _client.GetUserCartAsync(new GetUserCartRequest());
_items.Clear();
foreach (var item in response.Items)
{
_items.Add(new DiscountCartItem(
ProductId: item.ProductId,
Title: item.ProductTitle ?? string.Empty,
ImageUrl: item.ProductImagePath,
UnitPrice: item.UnitPrice,
MaxDiscountPercent: item.MaxDiscountPercent,
Quantity: item.Count,
TotalPrice: item.TotalPrice,
DiscountAmount: item.DiscountAmount,
FinalPrice: item.FinalPrice,
RemainingCount: item.ProductRemainingCount));
}
Notify();
}
catch { /* fallback to local state */ }
}
private void Notify() => OnChange?.Invoke();
private async Task<bool> IsAuthenticatedAsync()
{
try
{
var token = await _localStorage.GetItemAsync<string>(TokenStorageKey);
return !string.IsNullOrWhiteSpace(token);
}
catch { return false; }
}
}
@@ -0,0 +1,209 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using Google.Protobuf.WellKnownTypes;
using MudBlazor;
namespace FrontOffice.Main.Utilities;
// ── DTOs ──
public record DiscountOrderSummary(
long Id,
string OrderNumber,
long TotalPrice,
long DiscountBalanceUsed,
long GatewayAmount,
bool PaymentCompleted,
int DeliveryStatus,
string? TrackingCode,
int ItemsCount,
DateTime Created);
public record DiscountOrderDetail(
long Id,
string OrderNumber,
long TotalPrice,
long DiscountBalanceUsed,
long GatewayAmount,
bool PaymentCompleted,
string? TransactionId,
int DeliveryStatus,
string? TrackingCode,
string? Notes,
string? AdminNotes,
DiscountOrderAddress? Address,
List<DiscountOrderItem> Items,
DateTime Created);
public record DiscountOrderAddress(
long Id, string Title, string Address, string PostalCode, string? Phone);
public record DiscountOrderItem(
long ProductId,
string Title,
long UnitPrice,
int MaxDiscountPercent,
int Count,
long TotalPrice,
long DiscountAmount,
long FinalPrice);
public record PlaceDiscountOrderResult(
bool Success,
string Message,
long OrderId,
long GatewayAmount,
string? PaymentUrl);
public record DiscountOrderListResult(
List<DiscountOrderSummary> Orders,
int TotalCount,
int TotalPages,
int CurrentPage);
// ── Service ──
public class DiscountOrderService
{
private readonly DiscountOrderContract.DiscountOrderContractClient _client;
public DiscountOrderService(DiscountOrderContract.DiscountOrderContractClient client)
{
_client = client;
}
public async Task<PlaceDiscountOrderResult> PlaceOrderAsync(long addressId, long discountBalanceToUse, string? notes = null)
{
try
{
var request = new PlaceOrderRequest
{
UserAddressId = addressId,
DiscountBalanceToUse = discountBalanceToUse
};
if (!string.IsNullOrWhiteSpace(notes))
request.Notes = notes;
var response = await _client.PlaceOrderAsync(request);
return new PlaceDiscountOrderResult(
Success: response.Success,
Message: response.Message ?? string.Empty,
OrderId: response.OrderId,
GatewayAmount: response.GatewayAmount,
PaymentUrl: response.PaymentUrl);
}
catch (Exception ex)
{
return new PlaceDiscountOrderResult(false, ex.Message, 0, 0, null);
}
}
public async Task<bool> CompletePaymentAsync(long orderId, string? transactionId, bool success)
{
try
{
var response = await _client.CompleteOrderPaymentAsync(new CompleteOrderPaymentRequest
{
OrderId = orderId,
TransactionId = transactionId,
PaymentSuccess = success
});
return response.Success;
}
catch { return false; }
}
public async Task<DiscountOrderListResult> GetUserOrdersAsync(int page = 1, int pageSize = 10)
{
try
{
var response = await _client.GetUserOrdersAsync(new GetUserOrdersRequest
{
PageNumber = page,
PageSize = pageSize
});
var orders = response.Models.Select(m => new DiscountOrderSummary(
Id: m.Id,
OrderNumber: m.OrderNumber ?? string.Empty,
TotalPrice: m.TotalPrice,
DiscountBalanceUsed: m.DiscountBalanceUsed,
GatewayAmount: m.GatewayAmount,
PaymentCompleted: m.PaymentCompleted,
DeliveryStatus: (int)m.DeliveryStatus,
TrackingCode: m.TrackingCode,
ItemsCount: m.ItemsCount,
Created: m.Created?.ToDateTime() ?? DateTime.MinValue
)).ToList();
var totalCount = (int)(response.MetaData?.TotalCount ?? 0);
var totalPages = (int)(response.MetaData?.TotalPage ?? 0);
return new DiscountOrderListResult(orders, totalCount, totalPages, page);
}
catch
{
return new DiscountOrderListResult(new(), 0, 0, page);
}
}
public async Task<DiscountOrderDetail?> GetOrderByIdAsync(long orderId)
{
try
{
var r = await _client.GetOrderByIdAsync(new GetOrderByIdRequest { OrderId = orderId });
var address = r.Address != null
? new DiscountOrderAddress(r.Address.Id, r.Address.Title ?? "", r.Address.Address ?? "", r.Address.PostalCode ?? "", r.Address.Phone)
: null;
var items = r.Items.Select(i => new DiscountOrderItem(
ProductId: i.ProductId,
Title: i.ProductTitle ?? string.Empty,
UnitPrice: i.UnitPrice,
MaxDiscountPercent: i.MaxDiscountPercent,
Count: i.Count,
TotalPrice: i.TotalPrice,
DiscountAmount: i.DiscountAmount,
FinalPrice: i.FinalPrice
)).ToList();
return new DiscountOrderDetail(
Id: r.Id,
OrderNumber: r.OrderNumber ?? string.Empty,
TotalPrice: r.TotalPrice,
DiscountBalanceUsed: r.DiscountBalanceUsed,
GatewayAmount: r.GatewayAmount,
PaymentCompleted: r.PaymentCompleted,
TransactionId: r.TransactionId,
DeliveryStatus: (int)r.DeliveryStatus,
TrackingCode: r.TrackingCode,
Notes: r.Notes,
AdminNotes: r.AdminNotes,
Address: address,
Items: items,
Created: r.Created?.ToDateTime() ?? DateTime.MinValue);
}
catch
{
return null;
}
}
public static string GetDeliveryStatusText(int status) => status switch
{
0 => "در انتظار",
1 => "در حال پردازش",
2 => "ارسال شده",
3 => "تحویل داده شده",
4 => "لغو شده",
_ => "نامشخص"
};
public static Color GetDeliveryStatusColor(int status) => status switch
{
0 => Color.Warning,
1 => Color.Info,
2 => Color.Primary,
3 => Color.Success,
4 => Color.Error,
_ => Color.Default
};
}
@@ -0,0 +1,207 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using Google.Protobuf.WellKnownTypes;
namespace FrontOffice.Main.Utilities;
// ── DTOs ──
public record DiscountProductCard(
long Id,
string Title,
string ShortInfo,
long Price,
int MaxDiscountPercent,
string ImageUrl,
string ThumbnailUrl,
int RemainingCount,
int ViewCount,
bool IsActive,
DateTime Created);
public record DiscountProductDetail(
long Id,
string Title,
string ShortInfo,
string FullInfo,
long Price,
int MaxDiscountPercent,
string ImageUrl,
string ThumbnailUrl,
int RemainingCount,
int ViewCount,
int SortOrder,
bool IsActive,
List<DiscountCategoryInfo> Categories,
List<DiscountProductImageDto> Images,
DateTime Created);
public record DiscountCategoryInfo(long Id, string Name, string Title);
public record DiscountProductImageDto(
long Id,
string ImageUrl,
string ThumbnailUrl,
string? Title,
string? AltText,
int SortOrder);
public record DiscountCategoryNode(
long Id,
string Name,
string Title,
string? Description,
string? ImagePath,
long? ParentCategoryId,
int SortOrder,
bool IsActive,
int ProductCount,
List<DiscountCategoryNode> Children);
public record DiscountProductListResult(
List<DiscountProductCard> Products,
int TotalCount,
int TotalPages,
int CurrentPage);
// ── Service ──
public class DiscountProductService
{
private readonly DiscountProductContract.DiscountProductContractClient _productClient;
private readonly DiscountCategoryContract.DiscountCategoryContractClient _categoryClient;
public DiscountProductService(
DiscountProductContract.DiscountProductContractClient productClient,
DiscountCategoryContract.DiscountCategoryContractClient categoryClient)
{
_productClient = productClient;
_categoryClient = categoryClient;
}
public async Task<DiscountProductListResult> GetProductsAsync(
int page = 1, int pageSize = 12,
string? search = null, long? categoryId = null,
bool? inStock = null)
{
try
{
var request = new GetDiscountProductsRequest
{
PageNumber = page,
PageSize = pageSize,
IsActive = true // فقط محصولات فعال
};
if (!string.IsNullOrWhiteSpace(search))
request.SearchQuery = search;
if (categoryId.HasValue)
request.CategoryId = categoryId.Value;
if (inStock.HasValue)
request.InStock = inStock.Value;
var response = await _productClient.GetDiscountProductsAsync(request);
var products = response.Models.Select(m => new DiscountProductCard(
Id: m.Id,
Title: m.Title ?? string.Empty,
ShortInfo: m.ShortInfomation ?? string.Empty,
Price: m.Price,
MaxDiscountPercent: m.MaxDiscountPercent,
ImageUrl: BuildUrl(m.ImagePath),
ThumbnailUrl: BuildUrl(m.ThumbnailPath),
RemainingCount: m.RemainingCount,
ViewCount: m.ViewCount,
IsActive: m.IsActive,
Created: m.Created?.ToDateTime() ?? DateTime.MinValue
)).ToList();
var totalCount = (int)(response.MetaData?.TotalCount ?? 0);
var totalPages = (int)(response.MetaData?.TotalPage ?? 0);
return new DiscountProductListResult(products, totalCount, totalPages, page);
}
catch
{
return new DiscountProductListResult(new(), 0, 0, page);
}
}
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
{
try
{
var response = await _productClient.GetDiscountProductByIdAsync(
new GetDiscountProductByIdRequest { ProductId = productId });
var categories = response.Categories
.Select(c => new DiscountCategoryInfo(c.Id, c.Name ?? "", c.Title ?? ""))
.ToList();
// Load images
var imagesResponse = await _productClient.GetDiscountProductImagesAsync(
new GetDiscountProductImagesRequest { DiscountProductId = productId, OnlyActive = true });
var images = imagesResponse.Images
.OrderBy(i => i.SortOrder)
.Select(i => new DiscountProductImageDto(
Id: i.Id,
ImageUrl: BuildUrl(i.ImagePath),
ThumbnailUrl: BuildUrl(i.ThumbnailPath),
Title: i.Title,
AltText: i.AltText,
SortOrder: i.SortOrder))
.ToList();
return new DiscountProductDetail(
Id: response.Id,
Title: response.Title ?? string.Empty,
ShortInfo: response.ShortInfomation ?? string.Empty,
FullInfo: response.FullInformation ?? string.Empty,
Price: response.Price,
MaxDiscountPercent: response.MaxDiscountPercent,
ImageUrl: BuildUrl(response.ImagePath),
ThumbnailUrl: BuildUrl(response.ThumbnailPath),
RemainingCount: response.RemainingCount,
ViewCount: response.ViewCount,
SortOrder: response.SortOrder,
IsActive: response.IsActive,
Categories: categories,
Images: images,
Created: response.Created?.ToDateTime() ?? DateTime.MinValue);
}
catch
{
return null;
}
}
public async Task<List<DiscountCategoryNode>> GetCategoriesAsync(bool onlyActive = true)
{
try
{
var request = new GetDiscountCategoriesRequest();
if (onlyActive) request.IsActive = true;
var response = await _categoryClient.GetDiscountCategoriesAsync(request);
return response.Categories.Select(MapCategory).ToList();
}
catch
{
return new();
}
}
private static DiscountCategoryNode MapCategory(DiscountCategoryDto dto) => new(
Id: dto.Id,
Name: dto.Name ?? "",
Title: dto.Title ?? "",
Description: dto.Description,
ImagePath: dto.ImagePath,
ParentCategoryId: dto.ParentCategoryId,
SortOrder: dto.SortOrder,
IsActive: dto.IsActive,
ProductCount: dto.ProductCount,
Children: dto.Children.Select(MapCategory).ToList());
private static string BuildUrl(string? path)
=> path ?? string.Empty;
}
@@ -0,0 +1,122 @@
using System.Collections.Concurrent;
using CMSMicroservice.Protobuf.Protos.ImageResolver;
namespace FrontOffice.Main.Utilities;
/// <summary>
/// سرویس مرکزی resolve تصاویر — تمام تصاویر دینامیک FrontOffice از این سرویس عبور می‌کنند.
/// مسیرهای نسبی از طریق gRPC به CMS ارسال شده و base64 data-URI دریافت می‌شود.
/// نتایج در حافظه کش می‌شوند تا از فراخوانی مجدد جلوگیری شود.
/// </summary>
public class ImageCacheService
{
private readonly ImageResolverContract.ImageResolverContractClient _client;
private readonly ConcurrentDictionary<string, string> _cache = new();
public ImageCacheService(ImageResolverContract.ImageResolverContractClient client)
{
_client = client;
}
/// <summary>
/// تبدیل یک مسیر تصویر به base64 data-URI.
/// اگر مسیر خالی باشد → رشته خالی.
/// اگر قبلاً data-URI باشد → بدون تغییر.
/// اگر در کش باشد → از کش.
/// در غیر این صورت → فراخوانی gRPC.
/// </summary>
public async Task<string> ResolveAsync(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
// اگر قبلاً base64 هست (مثلاً از interceptor آمده)
if (path.StartsWith("data:", StringComparison.Ordinal))
return path;
// مسیرهای استاتیک (wwwroot) یا لینک‌های خارجی نیاز به resolve ندارند
if (path.StartsWith('/') || path.StartsWith("http://", StringComparison.Ordinal)
|| path.StartsWith("https://", StringComparison.Ordinal))
return path;
// جستجو در کش
if (_cache.TryGetValue(path, out var cached))
return cached;
try
{
var response = await _client.ResolveImagesAsync(
new ResolveImagesRequest { Paths = { path } });
var result = response?.Images?.FirstOrDefault()?.DataUri ?? string.Empty;
_cache.TryAdd(path, result);
return result;
}
catch
{
// در صورت خطا، رشته خالی برگردان تا UI خراب نشود
return string.Empty;
}
}
/// <summary>
/// تبدیل دسته‌ای مسیرهای تصویر به base64 data-URI.
/// مسیرهای کش‌شده و data-URI‌ها بدون فراخوانی gRPC برگردانده می‌شوند.
/// </summary>
public async Task<Dictionary<string, string>> ResolveBatchAsync(IEnumerable<string?> paths)
{
var result = new Dictionary<string, string>();
var toResolve = new List<string>();
foreach (var path in paths)
{
if (string.IsNullOrWhiteSpace(path))
continue;
if (path.StartsWith("data:", StringComparison.Ordinal)
|| path.StartsWith('/')
|| path.StartsWith("http://", StringComparison.Ordinal)
|| path.StartsWith("https://", StringComparison.Ordinal))
{
result[path] = path;
}
else if (_cache.TryGetValue(path, out var cached))
{
result[path] = cached;
}
else
{
toResolve.Add(path);
}
}
if (toResolve.Count == 0)
return result;
try
{
var response = await _client.ResolveImagesAsync(
new ResolveImagesRequest { Paths = { toResolve } });
if (response?.Images != null)
{
foreach (var img in response.Images)
{
_cache.TryAdd(img.OriginalPath, img.DataUri);
result[img.OriginalPath] = img.DataUri;
}
}
}
catch
{
// در صورت خطا، مسیرهای resolve نشده خالی بمانند
}
return result;
}
/// <summary>
/// پاکسازی تمام کش (مثلاً هنگام logout)
/// </summary>
public void ClearCache() => _cache.Clear();
}
@@ -44,7 +44,7 @@ public class OrderService
public async Task<List<GetUserOrderResponse>> GetOrdersAsync() public async Task<List<GetUserOrderResponse>> GetOrdersAsync()
{ {
var result = await _userOrderContractClient.GetAllUserOrderByFilterAsync(new()); var result = await _userOrderContractClient.GetCustomerOrdersAsync(new());
if (result != null && result.Models.Count > 0) if (result != null && result.Models.Count > 0)
{ {
foreach (var item in result.Models) foreach (var item in result.Models)
@@ -69,7 +69,7 @@ public class OrderService
var order = _orders.FirstOrDefault(o => o.Id == id); var order = _orders.FirstOrDefault(o => o.Id == id);
if (order == null) if (order == null)
{ {
var result = await _userOrderContractClient.GetUserOrderAsync(new GetUserOrderRequest() var result = await _userOrderContractClient.GetCustomerOrderAsync(new GetUserOrderRequest()
{ {
Id = id Id = id
}); });
@@ -54,7 +54,7 @@ public class PackageService
m.Id, m.Id,
m.Title, m.Title,
m.Description, m.Description,
UrlUtility.DownloadUrl + m.ImagePath, m.ImagePath,
m.Price)) m.Price))
.ToList(); .ToList();
} }
@@ -84,7 +84,7 @@ public class PackageService
response.Id, response.Id,
response.Title, response.Title,
response.Description, response.Description,
UrlUtility.DownloadUrl + response.ImagePath, response.ImagePath,
response.Price); response.Price);
} }
catch (Exception ex) catch (Exception ex)
@@ -141,7 +141,7 @@ public class ProductService
Title: m.Title ?? string.Empty, Title: m.Title ?? string.Empty,
Description: m.Description ?? string.Empty, Description: m.Description ?? string.Empty,
FullInformation: m.FullInformation ?? string.Empty, FullInformation: m.FullInformation ?? string.Empty,
ImageUrl: string.IsNullOrWhiteSpace(m.ImagePath) ? string.Empty : UrlUtility.DownloadUrl + m.ImagePath, ImageUrl: m.ImagePath,
Price: m.Price, Price: m.Price,
Discount: m.Discount, Discount: m.Discount,
Rate: m.Rate, Rate: m.Rate,
@@ -244,7 +244,7 @@ public class ProductService
private sealed record CacheEntry(Product Product, DateTime Expiration); private sealed record CacheEntry(Product Product, DateTime Expiration);
private static string BuildUrl(string? path) => private static string BuildUrl(string? path) =>
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path; path ?? string.Empty;
private static IReadOnlyList<ProductCategoryPathInfo> MapCategoryPaths(IEnumerable<ProductCategoryPath>? categories) private static IReadOnlyList<ProductCategoryPathInfo> MapCategoryPaths(IEnumerable<ProductCategoryPath>? categories)
{ {
@@ -15,6 +15,7 @@ public static class RouteConstants
public static class Profile public static class Profile
{ {
public const string Index = "/profile"; public const string Index = "/profile";
public const string Hub = "/profile/hub";
public const string Personal = "/profile/personal"; public const string Personal = "/profile/personal";
public const string Addresses = "/profile/addresses"; public const string Addresses = "/profile/addresses";
public const string Settings = "/profile/settings"; public const string Settings = "/profile/settings";
@@ -66,6 +67,12 @@ public static class RouteConstants
public const string Index = "/contact"; public const string Index = "/contact";
} }
public static class Blog
{
public const string Index = "/blog";
public const string Post = "/blog/"; // usage: /blog/{slug}
}
public static class Checkout public static class Checkout
{ {
public const string Index = "/checkout"; public const string Index = "/checkout";
@@ -82,4 +89,24 @@ public static class RouteConstants
public const string OrderTracking = "/order-tracking/"; // usage: /order-tracking/{id} public const string OrderTracking = "/order-tracking/"; // usage: /order-tracking/{id}
public const string Categories = "/categories"; public const string Categories = "/categories";
} }
public static class DiscountStore
{
public const string Products = "/discount-store";
public const string ProductDetail = "/discount-store/product/"; // usage: /discount-store/product/{id}
public const string Cart = "/discount-store/cart";
public const string Checkout = "/discount-store/checkout";
public const string Orders = "/discount-store/orders";
public const string OrderDetail = "/discount-store/order/"; // usage: /discount-store/order/{id}
}
/// <summary>
/// Gateway/chooser pages — when a concept exists in both stores
/// </summary>
public static class Gateway
{
public const string StoreChooser = "/stores";
public const string OrdersChooser = "/my-orders";
public const string CartChooser = "/my-cart";
}
} }
@@ -0,0 +1,114 @@
using System.Text.Json;
namespace FrontOffice.Main.Utilities;
public class SitePageService
{
private readonly CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient _client;
public SitePageService(CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient client)
{
_client = client;
}
public async Task<SitePageDto?> GetByKeyAsync(string pageKey)
{
try
{
var response = await _client.GetSitePageByKeyAsync(
new CMSMicroservice.Protobuf.Protos.SitePage.GetSitePageByKeyRequest { PageKey = pageKey });
if (response == null || response.Id <= 0) return null;
var dto = new SitePageDto
{
Id = response.Id,
PageKey = response.PageKey,
Title = response.Title,
MetaDescription = response.MetaDescription,
HeroTitle = response.HeroTitle,
HeroSubtitle = response.HeroSubtitle,
HeroImagePath = response.HeroImagePath,
IsActive = response.IsActive
};
foreach (var s in response.Sections.OrderBy(s => s.SortOrder))
{
dto.Sections.Add(new SitePageSectionDto
{
Id = s.Id,
SectionKey = s.SectionKey,
Title = s.Title,
Subtitle = s.Subtitle,
HtmlContent = s.HtmlContent,
IconName = s.IconName,
ImagePath = s.ImagePath,
ImageThumbnailPath = s.ImageThumbnailPath,
SortOrder = s.SortOrder,
IsActive = s.IsActive,
ExtraData = s.ExtraData
});
}
return dto;
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"SitePageService.GetByKeyAsync error: {ex.Message}");
#endif
return null;
}
}
}
public class SitePageDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public string? HeroImagePath { get; set; }
public bool IsActive { get; set; }
public List<SitePageSectionDto> Sections { get; set; } = new();
public SitePageSectionDto? GetSection(string sectionKey) =>
Sections.FirstOrDefault(s => s.SectionKey == sectionKey && s.IsActive);
public List<SitePageSectionDto> GetSections(string prefix) =>
Sections.Where(s => s.SectionKey.StartsWith(prefix) && s.IsActive)
.OrderBy(s => s.SortOrder).ToList();
}
public class SitePageSectionDto
{
public long Id { get; set; }
public string SectionKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Subtitle { get; set; }
public string? HtmlContent { get; set; }
public string? IconName { get; set; }
public string? ImagePath { get; set; }
public string? ImageThumbnailPath { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public string? ExtraData { get; set; }
public T? GetExtraData<T>() where T : class
{
if (string.IsNullOrWhiteSpace(ExtraData)) return null;
try
{
return JsonSerializer.Deserialize<T>(ExtraData, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch
{
return null;
}
}
}
@@ -3,4 +3,16 @@
public static class UrlUtility public static class UrlUtility
{ {
public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null
/// <summary>
/// ساخت URL کامل تصویر — اگر مسیر data-URI یا http باشد همان را برمی‌گرداند
/// </summary>
public static string GetImageUrl(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
if (path.StartsWith("data:") || path.StartsWith("http"))
return path;
return DownloadUrl + path;
}
} }

Some files were not shown because too many files have changed in this diff Show More