feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +1,101 @@
|
||||
@if (InlineMode)
|
||||
{
|
||||
@* Inline rendering without MudDialog wrapper *@
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
|
||||
@PhoneOrVerifyContent()
|
||||
</MudStack>
|
||||
<div class="auth-dialog-content">
|
||||
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="mb-4">
|
||||
<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">@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
|
||||
{
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
|
||||
</TitleContent>
|
||||
<MudDialog Class="auth-dialog-wrapper">
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
private readonly CreateNewOtpTokenRequest _phoneRequest = new();
|
||||
private MudForm? _phoneForm;
|
||||
private PhoneVerifyForm? _phoneVerifyForm;
|
||||
|
||||
private readonly VerifyOtpTokenRequest _verifyRequest = new();
|
||||
private MudForm? _verifyForm;
|
||||
|
||||
private bool _isBusy;
|
||||
private string? _phoneNumber;
|
||||
@@ -79,11 +78,12 @@ public partial class AuthDialog : IDisposable
|
||||
public async Task SendOtpAsync()
|
||||
{
|
||||
_errorMessage = null;
|
||||
if (_phoneForm is null)
|
||||
var phoneForm = _phoneVerifyForm?.GetPhoneForm();
|
||||
if (phoneForm is null)
|
||||
return;
|
||||
|
||||
await _phoneForm.Validate();
|
||||
if (!_phoneForm.IsValid)
|
||||
await phoneForm.Validate();
|
||||
if (!phoneForm.IsValid)
|
||||
return;
|
||||
|
||||
// if (EnableCaptcha)
|
||||
@@ -154,11 +154,12 @@ public partial class AuthDialog : IDisposable
|
||||
_errorMessage = null;
|
||||
_infoMessage = null;
|
||||
|
||||
if (_verifyForm is null)
|
||||
var verifyForm = _phoneVerifyForm?.GetVerifyForm();
|
||||
if (verifyForm is null)
|
||||
return false;
|
||||
|
||||
await _verifyForm.Validate();
|
||||
if (!_verifyForm.IsValid)
|
||||
await verifyForm.Validate();
|
||||
if (!verifyForm.IsValid)
|
||||
return false;
|
||||
|
||||
if (IsVerificationLocked)
|
||||
@@ -236,6 +237,10 @@ public partial class AuthDialog : IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
// اگه سرور RemainingAttempts برگردونده، از اون استفاده کن
|
||||
if (response.RemainingAttempts > 0)
|
||||
_attemptsLeft = response.RemainingAttempts;
|
||||
|
||||
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
|
||||
}
|
||||
catch (RpcException rpcEx)
|
||||
|
||||
@@ -135,7 +135,8 @@
|
||||
new CMSMicroservice.Protobuf.Protos.OtpToken.CreateNewOtpTokenRequest
|
||||
{
|
||||
Mobile = await GetUserMobileAsync(),
|
||||
Purpose = "ClubContract"
|
||||
Purpose = "signClubContract",
|
||||
SignGuid = _signGuid.ToString()
|
||||
});
|
||||
|
||||
if (response.Success)
|
||||
@@ -166,11 +167,9 @@
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
var userId = await GetCurrentUserIdAsync();
|
||||
var response = await ClubMembershipClient.AcceptClubMembershipContractAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.ClubMembership.AcceptClubMembershipContractRequest
|
||||
{
|
||||
UserId = userId,
|
||||
OtpCode = _otpCode,
|
||||
SignGuid = _signGuid.ToString(),
|
||||
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; } = "در حال بارگذاری...";
|
||||
}
|
||||
@@ -9,10 +9,10 @@
|
||||
<MudSnackbarProvider/>
|
||||
<MudLayout>
|
||||
<!-- 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">
|
||||
|
||||
<MudHidden Breakpoint="Breakpoint.SmAndUp">
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||
<MudIconButton Class="d-inline"
|
||||
Icon="@Icons.Material.Filled.Menu"
|
||||
OnClick="@ToggleDrawer"/>
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="d-flex align-center gap-2">
|
||||
<MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None">
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
<MudHidden Breakpoint="Breakpoint.SmAndUp" Invert="true">
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudImage ObjectFit="ObjectFit.Cover"
|
||||
ObjectPosition="ObjectPosition.Center"
|
||||
Width="32"
|
||||
@@ -32,37 +32,66 @@
|
||||
<MudText Typo="Typo.h6">کارا بازار سلامت</MudText>
|
||||
</MudStack>
|
||||
</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 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">
|
||||
محصولات
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات
|
||||
من
|
||||
</MudLink>
|
||||
@* ── Regular store nav ── *@
|
||||
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
|
||||
}
|
||||
<MudLink Href="@(RouteConstants.FAQ.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>
|
||||
else if (IsInDiscountStore && _isAuthenticated)
|
||||
{
|
||||
@* ── Discount store nav ── *@
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات تخفیفی</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</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 class="d-flex align-center gap-2">
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
@* <MudBadge Content="@_cartCount" Color="Color.Error" Overlap="true" Visible="@(_cartCount > 0)"> *@
|
||||
@* *@
|
||||
@* </MudBadge> *@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart" Color="@(_cartCount > 0?Color.Success:Color.Inherit)"
|
||||
Href="@(RouteConstants.Store.Cart)" />
|
||||
@* ── Cart icon: only visible when inside a store ── *@
|
||||
@if (IsInRegularStore)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
|
||||
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)
|
||||
{
|
||||
@@ -74,7 +103,10 @@
|
||||
</MudBadge>
|
||||
}
|
||||
<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>
|
||||
<MudDivider/>
|
||||
@@ -110,19 +142,49 @@
|
||||
<MudStack Spacing="2">
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">محصولات
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">سبد خرید
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">سفارشات من
|
||||
</MudLink>
|
||||
@if (IsInRegularStore)
|
||||
{
|
||||
<MudText Typo="Typo.overline" Class="mud-text-secondary">فروشگاه</MudText>
|
||||
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">محصولات</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Store.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 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>
|
||||
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
|
||||
بلاگ
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
|
||||
درباره ما
|
||||
</MudLink>
|
||||
@@ -165,13 +227,93 @@
|
||||
</MudStack>
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent>
|
||||
<MudMainContent Class="main-content-wrapper">
|
||||
@Body
|
||||
@if (DeviceDetector.IsDesktop())
|
||||
{
|
||||
<MudHidden Breakpoint="Breakpoint.SmAndDown">
|
||||
<Footer/>
|
||||
}
|
||||
</MudHidden>
|
||||
</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>
|
||||
</MudRTLProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Blazored.LocalStorage;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -16,6 +17,15 @@ public partial class MainLayout : IDisposable
|
||||
private bool _isAuthenticated;
|
||||
private string? _email;
|
||||
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;
|
||||
@@ -26,6 +36,7 @@ public partial class MainLayout : IDisposable
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private CartService CartService { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
||||
[Inject] private AppVersionService AppVersionService { get; set; } = default!;
|
||||
|
||||
private void ToggleTheme() => _isDark = !_isDark;
|
||||
@@ -35,20 +46,34 @@ public partial class MainLayout : IDisposable
|
||||
await JSRuntime.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
UpdateStoreContext();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("changeNavBgOnBodyScroll", "top", null, 1);
|
||||
await CheckAuthStatus();
|
||||
|
||||
await EnforceAuthGuardAsync();
|
||||
|
||||
if (_isAuthenticated)
|
||||
{
|
||||
// بررسی وضعیت ثبتنام کامل
|
||||
_isCompleteRegister = await AuthService.IsCompleteRegisterAsync();
|
||||
|
||||
// لود سبد خرید فقط برای کاربر لاگین شده
|
||||
await CartService.EnsureInitializedAsync();
|
||||
CartService.OnChange += OnCartChanged;
|
||||
_cartCount = CartService.Count;
|
||||
|
||||
await DiscountCartService.EnsureInitializedAsync();
|
||||
DiscountCartService.OnChange += OnDiscountCartChanged;
|
||||
_discountCartCount = DiscountCartService.Count;
|
||||
|
||||
// چک کردن بروزرسانی (بدون نمایش popup - فقط برای نشون دادن آیکون)
|
||||
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>
|
||||
/// چک کردن بروزرسانی بدون نمایش popup
|
||||
/// </summary>
|
||||
@@ -131,6 +233,12 @@ public partial class MainLayout : IDisposable
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void OnDiscountCartChanged()
|
||||
{
|
||||
_discountCartCount = DiscountCartService.Count;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CheckAuthStatus()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
@@ -154,6 +262,11 @@ public partial class MainLayout : IDisposable
|
||||
Navigation.NavigateTo(RouteConstants.Profile.Index);
|
||||
}
|
||||
|
||||
private void NavigateToHub()
|
||||
{
|
||||
Navigation.NavigateTo(RouteConstants.Profile.Hub);
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
await AuthService.LogoutAsync();
|
||||
@@ -165,5 +278,7 @@ public partial class MainLayout : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user