Update MudBlazor integration, improve captcha handling, and upgrade project dependencies

This commit is contained in:
masoodafar-web
2025-11-14 09:32:19 +03:30
parent cce59612fa
commit 07ea8f0f47
15 changed files with 456 additions and 395 deletions

1
.gitignore vendored
View File

@@ -491,3 +491,4 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
/src/.idea

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId>
@@ -14,11 +14,11 @@
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.115" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.114" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.112" />
<PackageReference Include="MudBlazor" Version="7.16.0" />
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.59.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.59.0" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,7 +10,7 @@
<MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3">
آماده شنیدن صدای شما هستیم
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" MaxWidth="600px">
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px">
سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.
</MudText>
</MudStack>
@@ -94,7 +94,7 @@
<MudItem xs="12">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Start">
<MudCheckBox T="bool" @bind-Checked="_contactForm.AcceptTerms" />
<MudCheckBox @bind-Value="_contactForm.AcceptTerms" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
با ارسال این فرم،
<MudLink Href="/privacy" Target="_blank">سیاست حفظ حریم خصوصی</MudLink>
@@ -234,4 +234,3 @@
</MudContainer>
</section>
</MudStack>

View File

@@ -193,7 +193,7 @@ else
</MudAvatar>
<div>
<MudText Typo="Typo.body2" >@(review.UserName)</MudText>
<MudRating ReadOnly="true" Value="review.Rating" Size="Size.Small" />
<MudRating ReadOnly="true" Value="@review.Rating" Size="Size.Small" />
</div>
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(review.Date)</MudText>

View File

@@ -8,7 +8,7 @@ namespace FrontOffice.Main.Pages.Profile.Components;
public partial class AddAddressDialog : ComponentBase
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private MudForm? _form;
@@ -41,5 +41,5 @@ public partial class AddAddressDialog : ComponentBase
}
}
private void Cancel() => MudDialog.Cancel();
private void Cancel() => MudDialog.Close(DialogResult.Cancel());
}

View File

@@ -9,8 +9,9 @@ namespace FrontOffice.Main.Pages.Profile.Components;
public partial class EditAddressDialog : ComponentBase
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; // updated type
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
// removed duplicate Snackbar injection; provided by Razor partial via _Imports
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; }
@@ -51,5 +52,5 @@ public partial class EditAddressDialog : ComponentBase
}
}
private void Cancel() => MudDialog.Cancel();
private void Cancel() => MudDialog.Close(DialogResult.Cancel());
}

View File

@@ -7,15 +7,14 @@
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="5">
<MudStack Spacing="2" Class="mb-6">
<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.h3" Class="mb-2">
فقط در چند دقیقه حساب خود را فعال کنید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد، درخواست خود را ارسال کنید.
اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد،
درخواست خود را ارسال کنید.
</MudText>
</MudStack>
@@ -42,15 +41,16 @@
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام را دنبال کنید.
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید.
</MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه اصلی</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Profile.Index))">مشاهده پروفایل</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
اصلی</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Profile.Index))">مشاهده پروفایل
</MudButton>
</MudStack>
</MudStack>
}
@@ -59,7 +59,8 @@
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h4">ویزارد ثبت‌نام</MudText>
<MudChip Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله</MudChip>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله
</MudChip>
</MudStack>
@if (_isSubmitting)
@@ -67,119 +68,80 @@
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
<MudStepper @bind-ActiveIndex="_activeStep"
Elevation="0"
DisableClick="true"
Class="mb-4">
<MudStep Label="تایید موبایل" Icon="@Icons.Material.Filled.Smartphone">
<MudForm @ref="_stepOneForm">
<MudTextField Label="شماره موبایل"
Placeholder="مثال: 09121234567"
InputType="InputType.Tel"
Immediate="true"
MaxLength="11"
Variant="Variant.Outlined"
@bind-Value="_model.MobileNumber"
For="@(() => _model.MobileNumber)"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Outlined.Phone" />
<MudStepper @bind-ActiveIndex="_activeStep" Elevation="0" DisableClick="true" Class="mb-4">
<ChildContent>
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
@* Inline AuthDialog with captcha enabled *@
<AuthDialog @ref="_authDialog" InlineMode="true" EnableCaptcha="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
</MudStep>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-4">
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center">
<MudText Typo="Typo.h5">@_captchaCode</MudText>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudForm @ref="_stepTwoForm">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.LastName" For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
</MudForm>
</MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره
کنید.
</MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
<MudList T="string" Dense="true">
<MudListItem T="string">استفاده از اطلاعات کاربری صرفاً برای ثبت نام و
احراز هویت مجاز است.</MudListItem>
<MudListItem T="string">تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران
انجام می‌شود.</MudListItem>
<MudListItem T="string">مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است.
</MudListItem>
</MudList>
</MudPaper>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
Disabled="_isSubmitting"
OnClick="GenerateCaptcha">تازه‌سازی کد</MudButton>
</MudStack>
<MudTextField Label="کد کپچا"
Placeholder="کد نمایش داده شده"
Immediate="true"
Variant="Variant.Outlined"
@bind-Value="_model.CaptchaInput"
For="@(() => _model.CaptchaInput)" />
</MudForm>
</MudStep>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود قرارداد نمونه
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: فایل متنی
</MudText>
</MudStack>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudForm @ref="_stepTwoForm">
<MudTextField Label="نام"
Variant="Variant.Outlined"
Immediate="true"
@bind-Value="_model.FirstName"
For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی"
Variant="Variant.Outlined"
Immediate="true"
@bind-Value="_model.LastName"
For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی"
Variant="Variant.Outlined"
Immediate="true"
MaxLength="10"
@bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)"
InputType="InputType.Number" />
</MudForm>
</MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined"
Severity="Severity.Info"
Class="mb-3">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره کنید.
</MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
<MudList Dense="true">
<MudListItem>استفاده از اطلاعات کاربری صرفاً برای ثبت نام و احراز هویت مجاز است.</MudListItem>
<MudListItem>تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران انجام می‌شود.</MudListItem>
<MudListItem>مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است.</MudListItem>
</MudList>
</MudPaper>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Download"
Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود قرارداد نمونه
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: فایل متنی</MudText>
</MudStack>
<MudCheckBox @bind-Checked="_model.AcceptTerms"
Color="Color.Success"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مققرات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
</ChildContent>
<ActionContent Context="stepper">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Text" Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(stepper); })">مرحله قبل</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="_isSubmitting"
OnClick="@(async () => { await GoNextAsync(stepper); })">
@_nextButtonText
</MudButton>
</MudStack>
</ActionContent>
</MudStepper>
<MudDivider Class="my-2" />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Text"
Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="GoBack">مرحله قبل</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Disabled="_isSubmitting"
OnClick="GoNextAsync">
@_nextButtonText
</MudButton>
</MudStack>
</MudStack>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
</section>

View File

@@ -1,4 +1,8 @@
using System.ComponentModel.DataAnnotations;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -6,14 +10,16 @@ namespace FrontOffice.Main.Pages;
public partial class RegisterWizard
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private readonly RegistrationModel _model = new();
private MudForm? _stepOneForm;
private MudForm? _stepTwoForm;
private MudForm? _stepThreeForm;
private int _activeStep;
private bool _isSubmitting;
private bool _completed;
private string _captchaCode = string.Empty;
private AuthDialog _authDialog;
private UpdateUserRequest _updateUserRequest = new();
private string _nextButtonText => _activeStep switch
{
@@ -25,25 +31,18 @@ public partial class RegisterWizard
protected override void OnInitialized()
{
base.OnInitialized();
GenerateCaptcha();
}
private void GenerateCaptcha()
{
var random = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
_captchaCode = random;
_model.CaptchaInput = string.Empty;
}
private void GoBack()
private async Task GoBack(MudStepper mudStepper)
{
if (_activeStep == 0 || _isSubmitting)
return;
_activeStep--;
await mudStepper.PreviousStepAsync();
}
private async Task GoNextAsync()
private async Task GoNextAsync(MudStepper mudStepper)
{
if (_isSubmitting)
return;
@@ -51,18 +50,31 @@ public partial class RegisterWizard
switch (_activeStep)
{
case 0:
if (!await ValidateStepAsync(_stepOneForm))
return;
if (!string.Equals(_model.CaptchaInput?.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase))
if (_authDialog._currentStep == AuthDialog.AuthStep.Phone)
{
Snackbar.Add("کد کپچا صحیح نیست.", Severity.Warning);
await _authDialog.SendOtpAsync();
return;
}
_activeStep = 1;
else
{
var verifyOtp = await _authDialog.VerifyOtpAsync();
if (!verifyOtp)
return;
_activeStep = 1;
}
break;
case 1:
if (!await ValidateStepAsync(_stepTwoForm))
return;
var saveResult = await SavePersonalInfo();
if (!saveResult)
return;
_activeStep = 2;
break;
case 2:
@@ -71,6 +83,8 @@ public partial class RegisterWizard
await SubmitAsync();
break;
}
await mudStepper.NextStepAsync();
}
private async Task<bool> ValidateStepAsync(MudForm? form)
@@ -102,15 +116,49 @@ public partial class RegisterWizard
Navigation.NavigateTo("docs/sample-contract.txt", true);
}
private void OnPhoneVerified()
{
// Move to next step after phone verification success
// _activeStep = 1;
// StateHasChanged();
}
private async Task<bool> SavePersonalInfo()
{
if (_stepTwoForm is null) return false;
await _stepTwoForm.Validate();
if (!_stepTwoForm.IsValid) return false;
try
{
// _updateUserRequest.AvatarPath="test";
// _updateUserRequest.BirthDate=Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc));
// _updateUserRequest.EmailNotifications = true;
// _updateUserRequest.PushNotifications = true;
// _updateUserRequest.SmsNotifications = true;
//
_updateUserRequest.FirstName = _model.FirstName;
_updateUserRequest.LastName = _model.LastName;
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();
await UserContract.UpdateUserAsync(request: _updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
return true;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره اطلاعات: {ex.Message}", Severity.Error);
return false;
}
finally
{
await InvokeAsync(StateHasChanged);
}
}
private sealed class RegistrationModel
{
[Required(ErrorMessage = "شماره موبایل الزامی است.")]
[RegularExpression(@"^09\d{9}$", ErrorMessage = "شماره موبایل معتبر نیست.")]
public string? MobileNumber { get; set; }
[Required(ErrorMessage = "کد کپچا را وارد کنید.")]
public string? CaptchaInput { get; set; }
[Required(ErrorMessage = "نام الزامی است.")]
[StringLength(50, ErrorMessage = "حداکثر ۵۰ کاراکتر")]
public string? FirstName { get; set; }
@@ -120,10 +168,10 @@ public partial class RegisterWizard
public string? LastName { get; set; }
[Required(ErrorMessage = "کد ملی الزامی است.")]
[RegularExpression(@"^\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")]
[RegularExpression("^\\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")]
public string? NationalCode { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "برای ادامه باید قوانین را تایید کنید.")]
public bool AcceptTerms { get; set; }
}
}
}

View File

@@ -15,7 +15,8 @@
<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="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<!-- Ensure latest MudBlazor CSS is used (cache-busting) -->
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
</head>
<body>
<component type="typeof(App)" render-mode="Server" />
@@ -31,8 +32,9 @@
<a class="dismiss">🗙</a>
</div>
<!-- Load MudBlazor JS before Blazor to avoid early JS interop calls failing; add cache-busting -->
<script src="_content/MudBlazor/MudBlazor.min.js" asp-append-version="true"></script>
<script src="_framework/blazor.server.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script>
// elementId: id نوار (مثلاً "top")
// containerSelector: کانتینری که اسکرول می‌خوره؛ برای MudLayout معمولا ".mud-main-content"
@@ -73,4 +75,3 @@
</script>
</body>
</html>

View File

@@ -23,7 +23,14 @@ ValidatorOptions.Global.LanguageManager = new CustomFluentValidationLanguageMana
#endregion
var appSettings = builder.Configuration.Get<AppSettings>();
UrlUtility.DownloadUrl = appSettings.DownloadUrl;
if (!string.IsNullOrWhiteSpace(appSettings?.DownloadUrl))
{
UrlUtility.DownloadUrl = appSettings.DownloadUrl;
}
else
{
UrlUtility.DownloadUrl = string.Empty; // fallback to empty
}
builder.Services.Configure<EncryptionSettings>(builder.Configuration.GetSection("EncryptionSettings"));
builder.Services.AddSingleton<MobileNumberEncryptor>();
@@ -52,10 +59,10 @@ app.Run();
public class AppSettings
{
public string DownloadUrl { get; set; }
public required string DownloadUrl { get; set; }
}
public class EncryptionSettings
{
public string Key { get; set; }
public string IV { get; set; }
public required string Key { get; set; }
public required string IV { get; set; }
}

View File

@@ -1,12 +1,63 @@
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
@if (InlineMode)
{
@* Inline rendering without MudDialog wrapper *@
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
@PhoneOrVerifyContent()
</MudStack>
}
else
{
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
</TitleContent>
<DialogContent>
@PhoneOrVerifyContent()
</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 -->
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.</MudText>
// 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" Validation="@(_phoneRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_phoneRequest.Mobile"
@@ -21,6 +72,21 @@
HelperText="مثال: 09121234567"
Class="mb-2" />
@if (EnableCaptcha)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-2 mb-2">
<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>
<MudButton Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy" OnClick="GenerateCaptcha">
تازه‌سازی کد
</MudButton>
</MudStack>
<MudTextField Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید." />
}
<MudCheckBox T="bool"
Label="شرایط و قوانین را می‌پذیرم"
Class="mb-1" />
@@ -33,7 +99,7 @@
}
else if (_currentStep == AuthStep.Verify)
{
<!-- Verify Step -->
// Verify Step
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را وارد کنید.</MudText>
<MudForm @ref="_verifyForm" Model="_verifyRequest" Validation="@(_verifyRequestValidator.ValidateValue)">
@@ -50,85 +116,28 @@
Class="mb-2"
MaxLength="6" />
@* <MudText Typo="Typo.body1" Align="Align.Center" Class="mb-2">
تلاش باقی‌مانده: @_attemptsLeft از @MaxVerificationAttempts
</MudText> *@
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error"
Dense="true"
Elevation="0"
Class="mb-2">
@(_errorMessage)
</MudAlert>
<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>
<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>
<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>
<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>
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">ارسال مجدد رمز پویا</MudButton>
}
</MudForm>
}
</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>
};
}

View File

@@ -1,20 +1,20 @@
using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using MudBlazor;
using MetadataAlias = Grpc.Core.Metadata; // resolve ambiguity with MudBlazor.Metadata
namespace FrontOffice.Main.Shared;
public partial class AuthDialog : IDisposable
{
[Parameter]
public bool HideCancelButton { get; set; } = false;
[Parameter] public bool HideCancelButton { get; set; }
[Parameter] public bool EnableCaptcha { get; set; }
[Parameter] public bool InlineMode { get; set; }
private enum AuthStep { Phone, Verify }
public enum AuthStep { Phone, Verify }
private const int DefaultResendCooldown = 120;
public const int MaxVerificationAttempts = 5;
private const string PhoneStorageKey = "auth:phone-number";
@@ -22,14 +22,14 @@ public partial class AuthDialog : IDisposable
private const string TokenStorageKey = "auth:token";
private const string OtpPurpose = "Login";
private AuthStep _currentStep = AuthStep.Phone;
public AuthStep _currentStep = AuthStep.Phone;
private CreateNewOtpTokenRequestValidator _phoneRequestValidator = new();
private CreateNewOtpTokenRequest _phoneRequest = new();
private readonly CreateNewOtpTokenRequestValidator _phoneRequestValidator = new();
private readonly CreateNewOtpTokenRequest _phoneRequest = new();
private MudForm? _phoneForm;
private VerifyOtpTokenRequestValidator _verifyRequestValidator = new();
private VerifyOtpTokenRequest _verifyRequest = new();
private readonly VerifyOtpTokenRequestValidator _verifyRequestValidator = new();
private readonly VerifyOtpTokenRequest _verifyRequest = new();
private MudForm? _verifyForm;
private bool _isBusy;
@@ -41,10 +41,14 @@ public partial class AuthDialog : IDisposable
private int _attemptsLeft = MaxVerificationAttempts;
private CancellationTokenSource? _operationCts;
// Captcha fields
private string? _captchaCode;
private string? _captchaInput;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[CascadingParameter] private IDialogReference? MudDialog { get; set; }
[Parameter] public EventCallback OnLoginSuccess { get; set; }
@@ -55,6 +59,11 @@ public partial class AuthDialog : IDisposable
_phoneRequest.Purpose = OtpPurpose;
_verifyRequest.Purpose = OtpPurpose;
if (EnableCaptcha)
{
GenerateCaptcha();
}
var storedPhone = await LocalStorage.GetItemAsync<string>(PhoneStorageKey);
if (!string.IsNullOrWhiteSpace(storedPhone))
{
@@ -62,7 +71,13 @@ public partial class AuthDialog : IDisposable
}
}
private async Task SendOtpAsync()
private void GenerateCaptcha()
{
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
_captchaInput = string.Empty;
}
public async Task SendOtpAsync()
{
_errorMessage = null;
if (_phoneForm is null)
@@ -72,6 +87,15 @@ public partial class AuthDialog : IDisposable
if (!_phoneForm.IsValid)
return;
if (EnableCaptcha)
{
if (string.IsNullOrWhiteSpace(_captchaInput) || !string.Equals(_captchaInput.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase))
{
_errorMessage = "کد کپچا صحیح نیست.";
return;
}
}
_isBusy = true;
_operationCts?.Cancel();
_operationCts?.Dispose();
@@ -87,15 +111,9 @@ public partial class AuthDialog : IDisposable
}
var metadata = await BuildAuthMetadataAsync();
CreateNewOtpTokenResponse response;
if (metadata is not null)
{
response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token);
}
else
{
response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token);
}
CreateNewOtpTokenResponse response = metadata is not null
? await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token)
: await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token);
if (response?.Success != true)
{
@@ -118,6 +136,7 @@ public partial class AuthDialog : IDisposable
}
catch (OperationCanceledException)
{
// ignored - user canceled operation
}
catch (Exception ex)
{
@@ -131,28 +150,28 @@ public partial class AuthDialog : IDisposable
}
}
private async Task VerifyOtpAsync()
public async Task<bool> VerifyOtpAsync()
{
_errorMessage = null;
_infoMessage = null;
if (_verifyForm is null)
return;
return false;
await _verifyForm.Validate();
if (!_verifyForm.IsValid)
return;
return false;
if (IsVerificationLocked)
{
_errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
return;
return false;
}
if (string.IsNullOrWhiteSpace(_phoneNumber))
{
_errorMessage = "شماره موبایل یافت نشد. لطفاً دوباره تلاش کنید.";
return;
return false;
}
_isBusy = true;
@@ -162,7 +181,6 @@ public partial class AuthDialog : IDisposable
{
_verifyRequest.Mobile = _phoneNumber;
// Check for stored referral code and add it to the request
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
if (!string.IsNullOrWhiteSpace(storedReferralCode))
{
@@ -173,24 +191,18 @@ public partial class AuthDialog : IDisposable
if (!validationResult.IsValid)
{
_errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
return;
return false;
}
var metadata = await BuildAuthMetadataAsync();
VerifyOtpTokenResponse response;
if (metadata is not null)
{
response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken);
}
else
{
response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken);
}
VerifyOtpTokenResponse response = metadata is not null
? await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken)
: await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken);
if (response is null)
{
_errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید.";
return;
return false;
}
if (response.Success)
@@ -206,16 +218,17 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
// Clear referral code after successful registration/login
await LocalStorage.RemoveItemAsync("referral:code");
_attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty;
await OnLoginSuccess.InvokeAsync();
MudDialog.Close();
return;
if (!InlineMode)
{
MudDialog?.Close();
}
return true;
}
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
@@ -226,6 +239,7 @@ public partial class AuthDialog : IDisposable
}
catch (OperationCanceledException)
{
// ignored - user canceled operation
}
catch (Exception ex)
{
@@ -237,6 +251,8 @@ public partial class AuthDialog : IDisposable
ClearOperationToken();
await InvokeAsync(StateHasChanged);
}
return false;
}
private async Task HandleVerificationFailureAsync(RpcException rpcEx)
@@ -269,23 +285,15 @@ public partial class AuthDialog : IDisposable
private void RegisterFailedAttempt(string baseMessage)
{
_attemptsLeft = Math.Max(0, _attemptsLeft - 1);
if (_attemptsLeft > 0)
{
_errorMessage = $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است.";
}
else
{
_errorMessage = $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
}
_errorMessage = _attemptsLeft > 0
? $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است."
: $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
}
private async Task ResendOtpAsync()
{
if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber))
{
return;
}
_errorMessage = null;
_infoMessage = null;
@@ -294,22 +302,11 @@ public partial class AuthDialog : IDisposable
try
{
var request = new CreateNewOtpTokenRequest
{
Mobile = _phoneNumber,
Purpose = OtpPurpose
};
var request = new CreateNewOtpTokenRequest { Mobile = _phoneNumber, Purpose = OtpPurpose };
var metadata = await BuildAuthMetadataAsync();
CreateNewOtpTokenResponse response;
if (metadata is not null)
{
response = await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken);
}
else
{
response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
}
CreateNewOtpTokenResponse response = metadata is not null
? await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken)
: await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
if (response?.Success != true)
{
@@ -319,10 +316,7 @@ public partial class AuthDialog : IDisposable
return;
}
_infoMessage = string.IsNullOrWhiteSpace(response.Message)
? "کد جدید ارسال شد."
: response.Message;
_infoMessage = string.IsNullOrWhiteSpace(response.Message) ? "کد جدید ارسال شد." : response.Message;
_attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty;
StartResendCountdown();
@@ -333,6 +327,7 @@ public partial class AuthDialog : IDisposable
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
@@ -373,6 +368,8 @@ public partial class AuthDialog : IDisposable
_resendTimer?.Dispose();
_resendTimer = null;
_resendRemaining = 0;
if (EnableCaptcha)
GenerateCaptcha();
}
private void StartResendCountdown(int seconds = DefaultResendCooldown)
@@ -388,23 +385,15 @@ public partial class AuthDialog : IDisposable
_resendTimer?.Dispose();
_resendTimer = null;
}
_ = InvokeAsync(StateHasChanged);
}, null, 1000, 1000);
}
private async Task<Metadata?> BuildAuthMetadataAsync()
private async Task<MetadataAlias?> BuildAuthMetadataAsync()
{
var token = await LocalStorage.GetItemAsync<string>(TokenStorageKey);
if (string.IsNullOrWhiteSpace(token))
{
return null;
}
return new Metadata
{
{ "Authorization", $"Bearer {token}" }
};
if (string.IsNullOrWhiteSpace(token)) return null;
return new MetadataAlias { { "Authorization", $"Bearer {token}" } };
}
private async Task ResetAuthenticationAsync()
@@ -430,7 +419,8 @@ public partial class AuthDialog : IDisposable
private void Cancel()
{
MudDialog.Cancel();
if (!InlineMode)
MudDialog?.Close(DialogResult.Cancel());
}
public void Dispose()
@@ -438,9 +428,9 @@ public partial class AuthDialog : IDisposable
_operationCts?.Cancel();
_operationCts?.Dispose();
_operationCts = null;
_resendTimer?.Dispose();
_resendTimer = null;
}
private string GetDialogTitle() => _currentStep == AuthStep.Phone ? "ورود/ثبت‌نام به حساب کاربری" : "تأیید رمز پویا";
}
}

View File

@@ -9,89 +9,82 @@ public static class CustomMudTheme
PaletteLight = new PaletteLight()
{
Primary = "#0380C0",
//Secondary = CustomColor.Secondary.Default,
//Tertiary = CustomColor.Tertiary.Default,
Background = "#F5F5F5",
AppbarBackground = "#F5F5F5",
//PrimaryContrastText = "#FFFFFF",
//SecondaryContrastText = "#FFFFFF",
//ErrorContrastText = "#FFFFFF",
//SuccessContrastText = "#FFFFFF",
//InfoContrastText = "#FFFFFF",
//WarningContrastText = "#FFFFFF",
TextPrimary = Colors.Gray.Darken3,
//// TextSecondary = "#FFFFFF",
//Error = CustomColor.Error.Default,
//ErrorLighten = CustomColor.Error.Error100,
//// Info = "#3977AD",
//InfoLighten = CustomColor.Info.Lighten,
//InfoDarken = CustomColor.Info.Darken,
//// Success = "#05AF82",
//SuccessDarken = CustomColor.Success.Darken,
//SuccessLighten = CustomColor.Success.Lighten,
////Warning = "#EF7300",
//WarningDarken = CustomColor.Warning.Lighten,
//WarningLighten = CustomColor.Warning.Lighten,
//BackgroundGrey = CustomColor.Other.Background,
//GrayDefault = CustomColor.Other.Background,
//GrayDark = CustomColor.Gray.Gray10,
//GrayLight = CustomColor.Gray.Gray60,
//GrayLighter = CustomColor.Gray.Gray80,
//TextDisabled = CustomColor.Other.DisableText,
//ActionDisabled = CustomColor.Other.DisableText,
//ActionDisabledBackground = CustomColor.Other.DisableBackground,
Surface = "#FFFFFF",
Divider = "#B2BFCB",
},
Typography = new Typography()
Typography = new Typography
{
// پایه
Default = new Default()
Default = new DefaultTypography()
{
FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" }
},
// هدینگ‌ها (اسکیل متعادل برای وب)
H1 = new H1 { FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = 1.70, FontWeight = 800, LetterSpacing = "normal" }, // ~32px
H2 = new H2 { FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = 1.65, FontWeight = 800, LetterSpacing = "normal" }, // ~30px
H3 = new H3 { FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = 1.60, FontWeight = 800, LetterSpacing = "normal" }, // ~24px
H4 = new H4 { FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = 1.55, FontWeight = 800, LetterSpacing = "normal" }, // ~20px
H5 = new H5 { FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = 1.50, FontWeight = 800, LetterSpacing = "normal" }, // ~18px
H6 = new H6 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.45, FontWeight = 800, LetterSpacing = "normal" }, // ~16px
// Subtitles
Subtitle1 = new Subtitle1 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.62, FontWeight = 500, LetterSpacing = "normal" },
Subtitle2 = new Subtitle2 { FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = 1.60, FontWeight = 500, LetterSpacing = "normal" },
// Body text (برای خوانایی بازتر از هدینگ‌ها)
Body1 = new Body1 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.85, FontWeight = 400, LetterSpacing = "normal" },
Body2 = new Body2 { FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = 1.80, FontWeight = 400, LetterSpacing = "normal" },
// Small text
Caption = new Caption { FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = 1.60, FontWeight = 400, LetterSpacing = "normal" },
Overline = new Overline { FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = 1.60, FontWeight = 500, LetterSpacing = "normal" },
// Buttons
Button = new Button
H1 = new H1Typography()
{
FontFamily = new[] { "Vazir" },
FontSize = "0.875rem",
LineHeight = 1.60,
FontWeight = 600,
LetterSpacing = "normal",
TextTransform = "none" // حروف بزرگ اجباری غیرفعال
FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = "1.70", FontWeight = "800",
LetterSpacing = "normal"
},
H2 = new H2Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = "1.65", FontWeight = "800",
LetterSpacing = "normal"
},
H3 = new H3Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = "1.60", FontWeight = "800",
LetterSpacing = "normal"
},
H4 = new H4Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = "1.55", FontWeight = "800",
LetterSpacing = "normal"
},
H5 = new H5Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = "1.50", FontWeight = "800",
LetterSpacing = "normal"
},
H6 = new H6Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.45", FontWeight = "800",
LetterSpacing = "normal"
},
Subtitle1 = new Subtitle1Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.62", FontWeight = "500",
LetterSpacing = "normal"
},
Subtitle2 = new Subtitle2Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "500",
LetterSpacing = "normal"
},
Body1 = new Body1Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.85", FontWeight = "400",
LetterSpacing = "normal"
},
Body2 = new Body2Typography()
{
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.80", FontWeight = "400",
LetterSpacing = "normal"
},
Caption = new CaptionTypography()
{
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "400",
LetterSpacing = "normal"
},
Overline = new OverlineTypography()
{
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "500",
LetterSpacing = "normal"
},
Button = new ButtonTypography()
{
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "600",
LetterSpacing = "normal", TextTransform = "none"
}
}
};

View File

@@ -2,5 +2,5 @@
public static class UrlUtility
{
public static string DownloadUrl { get; set; }
public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null
}

View File

@@ -5,7 +5,7 @@
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../fonts/Vazir-Light.ttf) format('woff2');
src: url(../fonts/Vazir-Light.ttf) format('truetype');
}
@font-face {
@@ -13,7 +13,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/Vazir-Regular.ttf) format('woff2');
src: url(../fonts/Vazir-Regular.ttf) format('truetype');
}
@font-face {
@@ -21,7 +21,7 @@
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/Vazir-Medium.ttf) format('woff2');
src: url(../fonts/Vazir-Medium.ttf) format('truetype');
}
@font-face {
@@ -29,7 +29,7 @@
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/Vazir-Bold.ttf) format('woff2');
src: url(../fonts/Vazir-Bold.ttf) format('truetype');
}
@font-face {
@@ -37,8 +37,58 @@
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(../fonts/Vazir-Bold.ttf) format('woff2');
src: url(../fonts/Vazir-Bold.ttf) format('truetype');
}
:root {
--app-font-family: 'Vazir', Tahoma, 'Segoe UI', Arial, sans-serif;
}
html, body {
font-family: var(--app-font-family);
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Apply Vazir to common Mud components */
.mud-typography,
.mud-typography-root,
.mud-button-label,
.mud-input-slot,
.mud-input-slot input,
.mud-input-slot textarea,
.mud-select,
.mud-snackbar,
.mud-chip,
.mud-breadcrumbs,
.mud-tooltip,
.mud-table,
.mud-textfield,
.mud-checkbox,
.mud-radio,
.mud-switch {
font-family: var(--app-font-family) !important;
}
/* Typography variants override to match previous settings */
.mud-typography-h1 { font-family: var(--app-font-family); font-size: 2rem; font-weight: 700; line-height: 1.70; letter-spacing: normal; }
.mud-typography-h2 { font-family: var(--app-font-family); font-size: 1.875rem; font-weight: 700; line-height: 1.65; letter-spacing: normal; }
.mud-typography-h3 { font-family: var(--app-font-family); font-size: 1.5rem; font-weight: 700; line-height: 1.60; letter-spacing: normal; }
.mud-typography-h4 { font-family: var(--app-font-family); font-size: 1.25rem; font-weight: 700; line-height: 1.55; letter-spacing: normal; }
.mud-typography-h5 { font-family: var(--app-font-family); font-size: 1.125rem; font-weight: 700; line-height: 1.50; letter-spacing: normal; }
.mud-typography-h6 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 700; line-height: 1.45; letter-spacing: normal; }
.mud-typography-subtitle1 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 500; line-height: 1.62; letter-spacing: normal; }
.mud-typography-subtitle2 { font-family: var(--app-font-family); font-size: 0.875rem; font-weight: 500; line-height: 1.60; letter-spacing: normal; }
.mud-typography-body1 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 400; line-height: 1.85; letter-spacing: normal; }
.mud-typography-body2 { font-family: var(--app-font-family); font-size: 0.875rem; font-weight: 400; line-height: 1.80; letter-spacing: normal; }
.mud-typography-caption { font-family: var(--app-font-family); font-size: 0.75rem; font-weight: 400; line-height: 1.60; letter-spacing: normal; }
.mud-typography-overline { font-family: var(--app-font-family); font-size: 0.75rem; font-weight: 500; line-height: 1.60; letter-spacing: normal; }
.mud-button-label { text-transform: none; }
/*#endregion*/
/*#region Layout Styles*/