From 52b8298a189458e0e6fb070610bb7bee009166b1 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Mon, 17 Nov 2025 02:53:51 +0330 Subject: [PATCH] Add OtpDialogService for mobile-friendly OTP authentication dialog --- src/FrontOffice.Main/ConfigureServices.cs | 5 + src/FrontOffice.Main/Pages/Index.razor.cs | 13 +- .../Pages/Profile/Addresses.razor | 73 ++++ .../Pages/Profile/Addresses.razor.cs | 90 +++++ .../Pages/Profile/Index.razor | 340 +++++------------- .../Pages/Profile/Index.razor.cs | 16 +- .../Pages/Profile/Personal.razor | 52 +++ .../Pages/Profile/Personal.razor.cs | 73 ++++ .../Pages/Profile/Settings.razor | 25 ++ .../Pages/Profile/Settings.razor.cs | 46 +++ src/FrontOffice.Main/Pages/Profile/Tree.razor | 17 + .../Pages/Profile/Wallet.razor | 66 ++++ .../Pages/Profile/Wallet.razor.cs | 22 ++ src/FrontOffice.Main/Pages/Store/Cart.razor | 80 +++++ .../Pages/Store/Cart.razor.cs | 38 ++ .../Pages/Store/CheckoutSummary.razor | 90 +++++ .../Pages/Store/CheckoutSummary.razor.cs | 81 +++++ .../Pages/Store/OrderDetail.razor | 79 ++++ .../Pages/Store/OrderDetail.razor.cs | 29 ++ src/FrontOffice.Main/Pages/Store/Orders.razor | 64 ++++ .../Pages/Store/Orders.razor.cs | 27 ++ .../Pages/Store/ProductDetail.razor | 45 +++ .../Pages/Store/ProductDetail.razor.cs | 31 ++ .../Pages/Store/Products.razor | 61 ++++ .../Pages/Store/Products.razor.cs | 45 +++ .../Shared/AuthDialog.razor.cs | 19 +- src/FrontOffice.Main/Shared/MainLayout.razor | 15 +- src/FrontOffice.Main/Utilities/AuthService.cs | 16 +- src/FrontOffice.Main/Utilities/CartService.cs | 61 ++++ .../Utilities/OrderService.cs | 47 +++ .../Utilities/ProductService.cs | 44 +++ .../Utilities/RouteConstants.cs | 15 + .../Utilities/WalletService.cs | 20 ++ src/FrontOffice.Main/wwwroot/css/site.css | 29 +- 34 files changed, 1495 insertions(+), 279 deletions(-) create mode 100644 src/FrontOffice.Main/Pages/Profile/Addresses.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Profile/Personal.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Personal.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Profile/Settings.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Settings.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Profile/Tree.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Wallet.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/Cart.razor create mode 100644 src/FrontOffice.Main/Pages/Store/Cart.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor create mode 100644 src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/OrderDetail.razor create mode 100644 src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/Orders.razor create mode 100644 src/FrontOffice.Main/Pages/Store/Orders.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/ProductDetail.razor create mode 100644 src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Store/Products.razor create mode 100644 src/FrontOffice.Main/Pages/Store/Products.razor.cs create mode 100644 src/FrontOffice.Main/Utilities/CartService.cs create mode 100644 src/FrontOffice.Main/Utilities/OrderService.cs create mode 100644 src/FrontOffice.Main/Utilities/ProductService.cs create mode 100644 src/FrontOffice.Main/Utilities/WalletService.cs diff --git a/src/FrontOffice.Main/ConfigureServices.cs b/src/FrontOffice.Main/ConfigureServices.cs index b92bb10..ba0b7fc 100644 --- a/src/FrontOffice.Main/ConfigureServices.cs +++ b/src/FrontOffice.Main/ConfigureServices.cs @@ -37,6 +37,11 @@ public static class ConfigureServices services.AddSingleton(); services.AddScoped(); services.AddScoped(); + // Storefront services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Device detection: very light, dependency-free services.AddTransient(); // PDF generation (Chromium only) diff --git a/src/FrontOffice.Main/Pages/Index.razor.cs b/src/FrontOffice.Main/Pages/Index.razor.cs index e3a2c75..43311ad 100644 --- a/src/FrontOffice.Main/Pages/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Index.razor.cs @@ -13,11 +13,22 @@ public partial class Index private string? _email; private bool _isLoadingPackages; private List _packs = new(); + [Inject] private AuthService AuthService { get; set; } = default!; protected override async Task OnInitializedAsync() { await LoadPackagesAsync(); - + if (await AuthService.IsAuthenticatedAsync()) + { + if ((await AuthService.IsCompleteRegisterAsync())) + { + Navigation.NavigateTo(RouteConstants.Profile.Index); + } + else + { + Navigation.NavigateTo(RouteConstants.Registration.Wizard); + } + } //string mobileNumber = "09387342688"; diff --git a/src/FrontOffice.Main/Pages/Profile/Addresses.razor b/src/FrontOffice.Main/Pages/Profile/Addresses.razor new file mode 100644 index 0000000..5748acc --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Addresses.razor @@ -0,0 +1,73 @@ +@attribute [Route(RouteConstants.Profile.Addresses)] + +آدرس‌ها + + + + + مدیریت آدرس‌ها + بازگشت + + + + + + آدرس‌های شما + افزودن آدرس جدید + + + @if (_isLoadingAddresses) + { + + + در حال بارگذاری آدرس‌ها... + + } + else if (_addresses.Any()) + { + + @foreach (var address in _addresses) + { + + + + + @address.Title + @if (address.IsDefault) + { + پیش‌فرض + } + + @address.Address + کد پستی: @address.PostalCode + + + + ویرایش + @if (!address.IsDefault) + { + تنظیم به عنوان پیش‌فرض + } + + حذف + + + + } + + } + else + { + + + هنوز آدرسی ثبت نکرده‌اید. + افزودن اولین آدرس + + } + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs b/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs new file mode 100644 index 0000000..69486c8 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs @@ -0,0 +1,90 @@ +using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress; +using FrontOffice.Main.Pages.Profile.Components; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class Addresses : ComponentBase +{ + [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + + private List _addresses = new(); + private bool _isLoadingAddresses; + + protected override async Task OnInitializedAsync() + { + await LoadAddresses(); + } + + private async Task LoadAddresses() + { + _isLoadingAddresses = true; + try + { + var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new()); + _addresses = response?.Models?.ToList() ?? new(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error); + } + finally + { + _isLoadingAddresses = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task OpenAddAddressDialog() + { + var dialog = await DialogService.ShowAsync("افزودن آدرس جدید"); + var result = await dialog.Result; + if (!result.Canceled) + await LoadAddresses(); + } + + private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address) + { + var dialog = await DialogService.ShowAsync("ویرایش آدرس", new DialogParameters + { + { x => x.Model, address } + }); + var result = await dialog.Result; + if (!result.Canceled) + await LoadAddresses(); + } + + private async Task SetAsDefaultAddress(long id) + { + try + { + await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = id }); + Snackbar.Add("آدرس پیش‌فرض تغییر کرد.", Severity.Success); + await LoadAddresses(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در تغییر آدرس پیش‌فرض: {ex.Message}", Severity.Error); + } + } + + private async Task DeleteAddress(long id) + { + var confirm = await DialogService.ShowMessageBox("تأیید حذف", "آیا از حذف این آدرس مطمئن هستید؟", yesText: "حذف", cancelText: "لغو"); + if (confirm == true) + { + try + { + await UserAddressContract.DeleteUserAddressAsync(new() { Id = id }); + Snackbar.Add("آدرس حذف شد.", Severity.Success); + await LoadAddresses(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در حذف آدرس: {ex.Message}", Severity.Error); + } + } + } +} + diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor b/src/FrontOffice.Main/Pages/Profile/Index.razor index f6358c6..03b28eb 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor @@ -62,263 +62,101 @@ - + - - - - -
- - - - - - - - - - - - - - - - - - - - - - ذخیره تغییرات - - - -
-
- - - -
- - - - آدرس‌های شما - - افزودن آدرس جدید - - - - - @if (_isLoadingAddresses) - { - - - در حال بارگذاری آدرس‌ها... - - } - else if (_addresses.Any()) - { - - @foreach (var address in _addresses) - { - - - - - - @address.Title - @if (address.IsDefault) - { - پیش‌فرض - } - - @(address.Address) - کد پستی: @(address.PostalCode) - - - - ویرایش - @if (!address.IsDefault) - { - تنظیم به عنوان پیش‌فرض - } - - حذف - - - - - } - - } - else - { - - - هنوز آدرسی ثبت نکرده‌اید. - - افزودن اولین آدرس - - - } - -
-
- - - -
- -
-
- - - -
- - - - - اعلان‌ها - - - - - - - @* - - - حریم خصوصی - - - - - - - - - زبان و تم - - - - فارسی - English - - - - - روشن - تیره - خودکار - - - - *@ - - - - ذخیره تنظیمات - - - -
-
- - - @* -
- آمار حساب کاربری - - - - - - @_userProfile.JoinDate - تاریخ عضویت - + + + + + موجودی اعتباری + @_walletCredit + - - - - - @_userProfile.LastLogin - آخرین ورود - + + + موجودی شبکه + @_walletNetwork + - - - - - @_userProfile.TotalReferrals - معرف‌ها - + + جزئیات کیف پول + + + - - - - @_userProfile.Level - سطح کاربری - + + + + + + + + + + اطلاعات شخصی + نمایش و ویرایش اطلاعات + + + - - - - - وضعیت حساب - - - - حساب تأیید شده - - - - ایمیل تأیید شده - - - - شماره موبایل تأیید شده - - - -
-
- *@ -
+ + + + + + آدرس‌ها + مدیریت آدرس‌های شما + + + + + + + + + + شجره‌نامه + ساختار شبکه + + + + + + + + + + تنظیمات حساب + اعلان‌ها و تنظیمات + + + + + + + + + + فروشگاه + مشاهده و خرید محصولات + + + + + + + + + + کیف پول + مدیریت و تاریخچه + + + + +
- \ No newline at end of file + diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs index 5805da2..34f3c6b 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs @@ -18,6 +18,7 @@ public partial class Index { [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + [Inject] private WalletService WalletService { get; set; } = default!; private GetUserResponse _userProfile = new(); private UpdateUserRequest _updateUserRequest = new(); @@ -46,9 +47,22 @@ public partial class Index { await LoadUserProfile(); await LoadAddresses(); + await LoadWallet(); } } + private string _walletCredit = "-"; + private string _walletNetwork = "-"; + private async Task LoadWallet() + { + var b = await WalletService.GetBalancesAsync(); + _walletCredit = FormatPrice(b.CreditBalance); + _walletNetwork = FormatPrice(b.NetworkBalance); + StateHasChanged(); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private async Task LoadUserProfile() { try @@ -286,4 +300,4 @@ public partial class Index var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364"; await JSRuntime.InvokeVoidAsync("open", url, "_blank"); } -} \ No newline at end of file +} diff --git a/src/FrontOffice.Main/Pages/Profile/Personal.razor b/src/FrontOffice.Main/Pages/Profile/Personal.razor new file mode 100644 index 0000000..0e72bbc --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Personal.razor @@ -0,0 +1,52 @@ +@attribute [Route(RouteConstants.Profile.Personal)] + +اطلاعات شخصی + + + + + اطلاعات شخصی + بازگشت + + + + + + + + + + + + + + + + + + + + + ذخیره تغییرات + + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Personal.razor.cs b/src/FrontOffice.Main/Pages/Profile/Personal.razor.cs new file mode 100644 index 0000000..6ad780b --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Personal.razor.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using FrontOffice.BFF.User.Protobuf.Protos.User; +using FrontOffice.BFF.User.Protobuf.Validator; +using FrontOffice.Main.Utilities; +using Mapster; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Severity = MudBlazor.Severity; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class Personal : ComponentBase +{ + [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; + + private GetUserResponse _userProfile = new(); + private UpdateUserRequest _updateUserRequest = new(); + private readonly UpdateUserRequestValidator _personalValidator = new(); + + private MudForm? _personalForm; + private DateTime? _date; + private bool _isPersonalSaving; + + protected override async Task OnInitializedAsync() + { + await LoadUserProfile(); + } + + private async Task LoadUserProfile() + { + try + { + _userProfile = await UserContract.GetUserAsync(new()); + _updateUserRequest = _userProfile.Adapt(); + if (_userProfile.BirthDate != null) + _date = _userProfile.BirthDate.ToDateTime(); + } + catch + { + _userProfile = new(); + } + } + + private async Task SavePersonalInfo() + { + if (_personalForm is null) return; + await _personalForm.Validate(); + if (!_personalForm.IsValid) return; + + _isPersonalSaving = true; + try + { + if (!string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode)) + _updateUserRequest.NationalCode = _updateUserRequest.NationalCode.PersianToEnglish(); + + if (_date != null) + _updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp(); + + await UserContract.UpdateUserAsync(_updateUserRequest); + Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در ذخیره اطلاعات: {ex.Message}", Severity.Error); + } + finally + { + _isPersonalSaving = false; + await InvokeAsync(StateHasChanged); + } + } +} + diff --git a/src/FrontOffice.Main/Pages/Profile/Settings.razor b/src/FrontOffice.Main/Pages/Profile/Settings.razor new file mode 100644 index 0000000..264c049 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Settings.razor @@ -0,0 +1,25 @@ +@attribute [Route(RouteConstants.Profile.Settings)] + +تنظیمات حساب + + + + + تنظیمات حساب + بازگشت + + + + + اعلان‌ها + + + + + + ذخیره تنظیمات + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs b/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs new file mode 100644 index 0000000..671fe41 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs @@ -0,0 +1,46 @@ +using FrontOffice.BFF.User.Protobuf.Protos.User; +using Mapster; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class Settings : ComponentBase +{ + [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; + + private UpdateUserRequest _request = new(); + private bool _saving; + + protected override async Task OnInitializedAsync() + { + try + { + var user = await UserContract.GetUserAsync(new()); + _request = user.Adapt(); + } + catch + { + _request = new(); + } + } + + private async Task SaveSettings() + { + _saving = true; + try + { + await UserContract.UpdateUserAsync(_request); + Snackbar.Add("تنظیمات ذخیره شد.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error); + } + finally + { + _saving = false; + } + } +} + diff --git a/src/FrontOffice.Main/Pages/Profile/Tree.razor b/src/FrontOffice.Main/Pages/Profile/Tree.razor new file mode 100644 index 0000000..bb50fbf --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Tree.razor @@ -0,0 +1,17 @@ +@attribute [Route(RouteConstants.Profile.Tree)] +@using FrontOffice.Main.Pages.Profile.Components + +شجره‌نامه + + + + + شجره‌نامه + بازگشت + + + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor b/src/FrontOffice.Main/Pages/Profile/Wallet.razor new file mode 100644 index 0000000..d672792 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor @@ -0,0 +1,66 @@ +@attribute [Route(RouteConstants.Profile.Wallet)] + +کیف پول + + + + + کیف پول + بازگشت + + + + + + موجودی اعتباری + @_balances.Credit + + + + + موجودی شبکه + @_balances.Network + + + + + + تراکنش‌ها و مسیرهای شارژ + + + + + تاریخ + مبلغ + مسیر + توضیحات + + + @context.Date.ToString("yyyy/MM/dd HH:mm") + @FormatPrice(context.Amount) + @context.Channel + @context.Description + + + + + + + @foreach (var tx in _txs) + { + + + + @tx.Date.ToString("yy/MM/dd HH:mm") + @FormatPrice(tx.Amount) + + @tx.Channel + @tx.Description + + + } + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs new file mode 100644 index 0000000..f22291c --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs @@ -0,0 +1,22 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class Wallet : ComponentBase +{ + [Inject] private WalletService WalletService { get; set; } = default!; + + private (string Credit, string Network) _balances = ("-", "-"); + private List _txs = new(); + + protected override async Task OnInitializedAsync() + { + var b = await WalletService.GetBalancesAsync(); + _balances = (FormatPrice(b.CreditBalance), FormatPrice(b.NetworkBalance)); + _txs = await WalletService.GetTransactionsAsync(); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} + diff --git a/src/FrontOffice.Main/Pages/Store/Cart.razor b/src/FrontOffice.Main/Pages/Store/Cart.razor new file mode 100644 index 0000000..f5b4312 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Cart.razor @@ -0,0 +1,80 @@ +@attribute [Route(RouteConstants.Store.Cart)] +@* Injection is handled in code-behind *@ + +سبد خرید + + + + سبد خرید + + @if (CartData.Items.Count == 0) + { + سبد خرید شما خالی است. + بازگشت به محصولات + } + else + { + + + + + محصول + قیمت واحد + تعداد + قیمت کل + + + + + + + @context.Title + + + @FormatPrice(context.UnitPrice) + + + + @FormatPrice(context.LineTotal) + + + + + + + + + + + @foreach (var item in CartData.Items) + { + + + + + @item.Title + + + @FormatPrice(item.UnitPrice) واحد + + + + جمع: @FormatPrice(item.LineTotal) + + + + + } + + + + + مبلغ قابل پرداخت: @FormatPrice(CartData.Total) + + افزودن محصول + ادامه فرایند خرید + + + } + + diff --git a/src/FrontOffice.Main/Pages/Store/Cart.razor.cs b/src/FrontOffice.Main/Pages/Store/Cart.razor.cs new file mode 100644 index 0000000..3c40f6c --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Cart.razor.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Components; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.Store; + +public partial class Cart : ComponentBase, IDisposable +{ + [Inject] private CartService CartService { get; set; } = default!; + // Navigation and Snackbar are available via _Imports.razor + private CartService CartData => CartService; + + protected override void OnInitialized() + { + CartService.OnChange += StateHasChanged; + } + + private void ChangeQty(long productId, int value) + { + CartService.UpdateQuantity(productId, value); + } + + private void Remove(long productId) + { + CartService.Remove(productId); + } + + private void ProceedCheckout() + { + Navigation.NavigateTo(RouteConstants.Store.CheckoutSummary); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + + public void Dispose() + { + CartService.OnChange -= StateHasChanged; + } +} diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor new file mode 100644 index 0000000..e1615ca --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor @@ -0,0 +1,90 @@ +@attribute [Route(RouteConstants.Store.CheckoutSummary)] + +خلاصه خرید + + + + + + انتخاب آدرس + + @if (_loadingAddresses) + { + + + در حال بارگذاری آدرس‌ها... + + } + else if (_addresses.Count == 0) + { + + هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید. + + افزودن آدرس + } + else + { + + @foreach (var address in _addresses) + { + + + + @address.Title + @address.Address + + @if (address.IsDefault) + { + پیش‌فرض + } + + + } + + } + + + + روش پرداخت + + کیف پول + + در حال حاضر تنها پرداخت از طریق کیف پول فعال است. + + + + + + خلاصه سفارش + @if (Cart.Items.Count == 0) + { + سبد خرید شما خالی است. + } + else + { + + @foreach (var item in Cart.Items) + { + + + @item.Title x @item.Quantity + @FormatPrice(item.LineTotal) + + + } + + + + مبلغ قابل پرداخت + @FormatPrice(Cart.Total) + + ثبت سفارش + } + + + + diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs new file mode 100644 index 0000000..84bcff9 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs @@ -0,0 +1,81 @@ +using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress; +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Store; + +public partial class CheckoutSummary : ComponentBase +{ + [Inject] private CartService Cart { get; set; } = default!; + [Inject] private OrderService OrderService { get; set; } = default!; + [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + // Snackbar and Navigation are injected via _Imports.razor + + private List _addresses = new(); + private GetAllUserAddressByFilterResponseModel? _selectedAddress; + private bool _loadingAddresses; + + private PaymentMethod _payment = PaymentMethod.Wallet; + + private bool CanPlaceOrder => Cart.Items.Count > 0 && _selectedAddress != null; + + protected override async Task OnInitializedAsync() + { + await LoadAddresses(); + } + + private async Task LoadAddresses() + { + _loadingAddresses = true; + try + { + var response = await UserAddressContract.GetAllUserAddressByFilterAsync(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; + } + + // Simulate wallet payment success and create local order + var order = new Order + { + Status = OrderStatus.Paid, + PaymentMethod = _payment, + AddressId = _selectedAddress.Id, + AddressSummary = _selectedAddress.Address, + Items = Cart.Items.Select(i => new OrderItem(i.ProductId, i.Title, i.ImageUrl, i.UnitPrice, i.Quantity)).ToList() + }; + + var id = await OrderService.CreateOrderAsync(order); + Cart.Clear(); + Snackbar.Add("سفارش با موفقیت ثبت شد.", Severity.Success); + Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{id}"); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} diff --git a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor new file mode 100644 index 0000000..223a72d --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor @@ -0,0 +1,79 @@ +@attribute [Route(RouteConstants.Store.OrderDetail + "{id:long}")] +@* Injection is handled in code-behind *@ + +جزئیات سفارش + +@if (_loading) +{ + + + + + +} +else if (_order is null) +{ + + سفارش یافت نشد. + بازگشت + +} +else +{ + + + + سفارش #@_order.Id + تاریخ: @_order.CreatedAt.ToString("yyyy/MM/dd HH:mm") + وضعیت: @GetStatusText(_order.Status) + روش پرداخت: کیف پول + آدرس: @_order.AddressSummary + + + + + محصول + قیمت واحد + تعداد + قیمت کل + + + + + + @context.Title + + + @FormatPrice(context.UnitPrice) + @context.Quantity + @FormatPrice(context.LineTotal) + + + + + + + @foreach (var it in _order.Items) + { + + + + + @it.Title + + + @FormatPrice(it.UnitPrice) واحد + تعداد: @it.Quantity + + جمع: @FormatPrice(it.LineTotal) + + + } + + + + مبلغ کل: @FormatPrice(_order.Total) + + + +} diff --git a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs new file mode 100644 index 0000000..4415e51 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs @@ -0,0 +1,29 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; + +namespace FrontOffice.Main.Pages.Store; + +public partial class OrderDetail : ComponentBase +{ + [Inject] private OrderService OrderService { get; set; } = default!; + + [Parameter] public long id { get; set; } + + private Order? _order; + private bool _loading; + + protected override async Task OnParametersSetAsync() + { + _loading = true; + _order = await OrderService.GetOrderAsync(id); + _loading = false; + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private static string GetStatusText(OrderStatus status) => status switch + { + OrderStatus.Paid => "پرداخت‌شده", + _ => "در انتظار", + }; +} + diff --git a/src/FrontOffice.Main/Pages/Store/Orders.razor b/src/FrontOffice.Main/Pages/Store/Orders.razor new file mode 100644 index 0000000..de4afe8 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Orders.razor @@ -0,0 +1,64 @@ +@attribute [Route(RouteConstants.Store.Orders)] +@* Injection is handled in code-behind *@ + +سفارشات من + + + سفارشات من + @if (_loading) + { + + + + } + else if (_orders.Count == 0) + { + هنوز سفارشی ثبت نکرده‌اید. + } + else + { + + + + شناسه + تاریخ + وضعیت + مبلغ + + + + @context.Id + @context.CreatedAt.ToString("yyyy/MM/dd HH:mm") + @GetStatusText(context.Status) + @FormatPrice(context.Total) + + جزئیات + + + + + + + + @foreach (var o in _orders) + { + + + + سفارش #@o.Id + @o.CreatedAt.ToString("yy/MM/dd HH:mm") + + + وضعیت: @GetStatusText(o.Status) + @FormatPrice(o.Total) + + + جزئیات + + + + } + + + } + diff --git a/src/FrontOffice.Main/Pages/Store/Orders.razor.cs b/src/FrontOffice.Main/Pages/Store/Orders.razor.cs new file mode 100644 index 0000000..f24289a --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Orders.razor.cs @@ -0,0 +1,27 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; + +namespace FrontOffice.Main.Pages.Store; + +public partial class Orders : ComponentBase +{ + [Inject] private OrderService OrderService { get; set; } = default!; + + private List _orders = new(); + private bool _loading; + + protected override async Task OnInitializedAsync() + { + _loading = true; + _orders = await OrderService.GetOrdersAsync(); + _loading = false; + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private static string GetStatusText(OrderStatus status) => status switch + { + OrderStatus.Paid => "پرداخت‌شده", + _ => "در انتظار", + }; +} + diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor new file mode 100644 index 0000000..d1269c8 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor @@ -0,0 +1,45 @@ +@attribute [Route(RouteConstants.Store.ProductDetail + "{id:long}")] +@* Injection is handled in code-behind *@ + +جزئیات محصول + +@if (_loading) +{ + + + + + +} +else if (_product is null) +{ + + محصول یافت نشد. + بازگشت + +} +else +{ + + + + + + + + + + @_product.Title + @_product.Description + + @FormatPrice(_product.Price) + + + افزودن به سبد + مشاهده سبد + + + + + +} diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs new file mode 100644 index 0000000..f498086 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Components; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.Store; + +public partial class ProductDetail : ComponentBase +{ + [Inject] private ProductService ProductService { get; set; } = default!; + [Inject] private CartService Cart { get; set; } = default!; + + [Parameter] public long id { get; set; } + + private Product? _product; + private bool _loading; + private int _qty = 1; + + protected override async Task OnParametersSetAsync() + { + _loading = true; + _product = await ProductService.GetByIdAsync(id); + _loading = false; + } + + private void AddToCart() + { + if (_product is null) return; + Cart.Add(_product, _qty); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} diff --git a/src/FrontOffice.Main/Pages/Store/Products.razor b/src/FrontOffice.Main/Pages/Store/Products.razor new file mode 100644 index 0000000..cca4b83 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Products.razor @@ -0,0 +1,61 @@ +@attribute [Route(RouteConstants.Store.Products)] +@* Injection is handled in code-behind *@ + +محصولات + + + + + + + + + + سبد خرید (@Cart.Count) + + + + + + @if (_loading) + { + + + در حال بارگذاری... + + } + else if (_products.Count == 0) + { + محصولی یافت نشد. + } + else + { + + @foreach (var p in _products) + { + + + + + + + @p.Title + @p.Description + @FormatPrice(p.Price) + + + جزئیات + افزودن + + + + } + + } + diff --git a/src/FrontOffice.Main/Pages/Store/Products.razor.cs b/src/FrontOffice.Main/Pages/Store/Products.razor.cs new file mode 100644 index 0000000..b8fad02 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Store/Products.razor.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.Store; + +public partial class Products : ComponentBase, IDisposable +{ + [Inject] private ProductService ProductService { get; set; } = default!; + [Inject] private CartService Cart { get; set; } = default!; + + private string _query = string.Empty; + private bool _loading; + private List _products = new(); + + protected override async Task OnInitializedAsync() + { + Cart.OnChange += StateHasChanged; + await Load(); + } + + private async Task Load() + { + _loading = true; + _products = await ProductService.GetProductsAsync(_query); + _loading = false; + } + + private async Task OnQueryChanged(KeyboardEventArgs _) + { + await Load(); + } + + private void AddToCart(Product p) + { + Cart.Add(p, 1); + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + + public void Dispose() + { + Cart.OnChange -= StateHasChanged; + } +} diff --git a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs index 3554ba1..c12ba4a 100644 --- a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs +++ b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs @@ -48,7 +48,6 @@ public partial class AuthDialog : IDisposable [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; [Inject] private UserContract.UserContractClient UserClient { get; set; } = default!; - [Inject] private AuthService AuthService { get; set; } = default!; [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } @@ -66,7 +65,6 @@ public partial class AuthDialog : IDisposable // { GenerateCaptcha(); // } - var storedPhone = await LocalStorage.GetItemAsync(PhoneStorageKey); if (!string.IsNullOrWhiteSpace(storedPhone)) { @@ -227,22 +225,14 @@ public partial class AuthDialog : IDisposable _attemptsLeft = MaxVerificationAttempts; _verifyRequest.Code = string.Empty; - await OnLoginSuccess.InvokeAsync(); + + if (!InlineMode) { MudDialog?.Close(); } - if (await AuthService.IsAuthenticatedAsync()) - { - if (!(await AuthService.IsCompleteRegisterAsync())) - { - Navigation.NavigateTo(RouteConstants.Registration.Wizard); - } - else - { - Navigation.NavigateTo(RouteConstants.Profile.Index); - } - } + await OnLoginSuccess.InvokeAsync(); + // await OnLoginSuccessAsync(); return true; } @@ -270,6 +260,7 @@ public partial class AuthDialog : IDisposable return false; } + private async Task HandleVerificationFailureAsync(RpcException rpcEx) { switch (rpcEx.Status.StatusCode) diff --git a/src/FrontOffice.Main/Shared/MainLayout.razor b/src/FrontOffice.Main/Shared/MainLayout.razor index 77024f2..15c90e7 100644 --- a/src/FrontOffice.Main/Shared/MainLayout.razor +++ b/src/FrontOffice.Main/Shared/MainLayout.razor @@ -35,6 +35,12 @@
+ @if (_isAuthenticated) + { + محصولات + سبد خرید + سفارشات من + } سوالات متداول ارتباط با ما درباره ما @@ -44,7 +50,7 @@ @if (_isAuthenticated) { - پروفایل + پروفایل خروج از حساب @@ -63,6 +69,12 @@ + @if (_isAuthenticated) + { + محصولات + سبد خرید + سفارشات من + } سوالات متداول درباره ما ارتباط با ما @@ -87,4 +99,3 @@ - diff --git a/src/FrontOffice.Main/Utilities/AuthService.cs b/src/FrontOffice.Main/Utilities/AuthService.cs index c752433..8aa08a9 100644 --- a/src/FrontOffice.Main/Utilities/AuthService.cs +++ b/src/FrontOffice.Main/Utilities/AuthService.cs @@ -35,20 +35,20 @@ public class AuthService public async Task IsCompleteRegisterAsync() { await InitUserAuthInfo(); - if (_userAuthInfo.NationalCode ==null || _userAuthInfo.FirstName == null || _userAuthInfo.LastName == null || !_userAuthInfo.IsSignMainContract) + if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract) { - return false; + return true; } - return true; + return false; } public bool IsCompleteRegister() { InitUserAuthInfo().GetAwaiter(); - if (_userAuthInfo.NationalCode ==null || _userAuthInfo.FirstName == null || _userAuthInfo.LastName == null || _userAuthInfo.IsSignMainContract) - { - return false; - } - return true; + if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract) + { + return true; + } + return false; } public async Task GetUserAuthInfo() { diff --git a/src/FrontOffice.Main/Utilities/CartService.cs b/src/FrontOffice.Main/Utilities/CartService.cs new file mode 100644 index 0000000..4902b5c --- /dev/null +++ b/src/FrontOffice.Main/Utilities/CartService.cs @@ -0,0 +1,61 @@ +namespace FrontOffice.Main.Utilities; + +public record CartItem(long ProductId, string Title, string ImageUrl, long UnitPrice, int Quantity) +{ + public long LineTotal => UnitPrice * Quantity; +} + +public class CartService +{ + private readonly List _items = new(); + public event Action? OnChange; + + public IReadOnlyList Items => _items.AsReadOnly(); + public long Total => _items.Sum(i => i.LineTotal); + public int Count => _items.Sum(i => i.Quantity); + + public void Add(Product product, int quantity = 1) + { + if (quantity <= 0) return; + var existing = _items.FirstOrDefault(i => i.ProductId == product.Id); + if (existing is null) + { + _items.Add(new CartItem(product.Id, product.Title, product.ImageUrl, product.Price, quantity)); + } + else + { + var idx = _items.IndexOf(existing); + _items[idx] = existing with { Quantity = existing.Quantity + quantity }; + } + Notify(); + } + + public void UpdateQuantity(long productId, int quantity) + { + var existing = _items.FirstOrDefault(i => i.ProductId == productId); + if (existing is null) return; + if (quantity <= 0) + { + Remove(productId); + return; + } + var idx = _items.IndexOf(existing); + _items[idx] = existing with { Quantity = quantity }; + Notify(); + } + + public void Remove(long productId) + { + _items.RemoveAll(i => i.ProductId == productId); + Notify(); + } + + public void Clear() + { + _items.Clear(); + Notify(); + } + + private void Notify() => OnChange?.Invoke(); +} + diff --git a/src/FrontOffice.Main/Utilities/OrderService.cs b/src/FrontOffice.Main/Utilities/OrderService.cs new file mode 100644 index 0000000..9b02ca4 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/OrderService.cs @@ -0,0 +1,47 @@ +namespace FrontOffice.Main.Utilities; + +public enum PaymentMethod +{ + Wallet = 1, +} + +public enum OrderStatus +{ + Pending = 0, + Paid = 1, +} + +public record OrderItem(long ProductId, string Title, string ImageUrl, long UnitPrice, int Quantity) +{ + public long LineTotal => UnitPrice * Quantity; +} + +public class Order +{ + public long Id { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.Now; + public OrderStatus Status { get; set; } = OrderStatus.Pending; + public PaymentMethod PaymentMethod { get; set; } = PaymentMethod.Wallet; + public long AddressId { get; set; } + public string AddressSummary { get; set; } = string.Empty; + public List Items { get; set; } = new(); + public long Total => Items.Sum(i => i.LineTotal); +} + +public class OrderService +{ + private long _seq = 1000; + private readonly List _orders = new(); + + public Task> GetOrdersAsync() => Task.FromResult(_orders.OrderByDescending(o => o.CreatedAt).ToList()); + + public Task GetOrderAsync(long id) => Task.FromResult(_orders.FirstOrDefault(o => o.Id == id)); + + public Task CreateOrderAsync(Order order) + { + order.Id = Interlocked.Increment(ref _seq); + _orders.Add(order); + return Task.FromResult(order.Id); + } +} + diff --git a/src/FrontOffice.Main/Utilities/ProductService.cs b/src/FrontOffice.Main/Utilities/ProductService.cs new file mode 100644 index 0000000..37ac5e0 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/ProductService.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; + +namespace FrontOffice.Main.Utilities; + +public record Product(long Id, string Title, string Description, string ImageUrl, long Price); + +public class ProductService +{ + private static readonly List _seed = new() + { + new(1, "ماسک پزشکی سه‌لایه", "ماسک سه‌لایه با فیلتراسیون بالا مناسب برای محیط‌های عمومی.", "/images/store/mask.jpg", 49000), + new(2, "دستکش لاتکس", "دستکش لاتکس مناسب معاینات پزشکی، بدون پودر.", "/images/store/gloves.jpg", 89000), + new(3, "محلول ضدعفونی کننده", "محلول ضدعفونی بر پایه الکل ۷۰٪ مناسب دست و سطوح.", "/images/store/sanitizer.jpg", 69000), + new(4, "تب‌سنج دیجیتال", "تب‌سنج دیجیتال با دقت بالا و نمایشگر LCD.", "/images/store/thermometer.jpg", 299000), + new(5, "فشارسنج دیجیتال", "فشارسنج بازویی دیجیتال با حافظه داخلی.", "/images/store/bp-monitor.jpg", 1259000) + }; + + private readonly ConcurrentDictionary _products = new(); + + public ProductService() + { + foreach (var p in _seed) _products[p.Id] = p; + } + + public Task> GetProductsAsync(string? query = null) + { + IEnumerable list = _products.Values.OrderBy(p => p.Id); + if (!string.IsNullOrWhiteSpace(query)) + { + query = query.Trim(); + list = list.Where(p => + p.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || + p.Description.Contains(query, StringComparison.OrdinalIgnoreCase)); + } + return Task.FromResult(list.ToList()); + } + + public Task GetByIdAsync(long id) + { + _products.TryGetValue(id, out var result); + return Task.FromResult(result); + } +} + diff --git a/src/FrontOffice.Main/Utilities/RouteConstants.cs b/src/FrontOffice.Main/Utilities/RouteConstants.cs index 1b50150..c3d1950 100644 --- a/src/FrontOffice.Main/Utilities/RouteConstants.cs +++ b/src/FrontOffice.Main/Utilities/RouteConstants.cs @@ -15,6 +15,11 @@ public static class RouteConstants public static class Profile { public const string Index = "/profile"; + public const string Personal = "/profile/personal"; + public const string Addresses = "/profile/addresses"; + public const string Settings = "/profile/settings"; + public const string Tree = "/profile/tree"; + public const string Wallet = "/profile/wallet"; } public static class Package @@ -41,4 +46,14 @@ public static class RouteConstants { public const string Index = "/checkout"; } + + public static class Store + { + public const string Products = "/products"; + public const string ProductDetail = "/product/"; // usage: /product/{id} + public const string Cart = "/cart"; + public const string CheckoutSummary = "/checkout-summary"; + public const string Orders = "/orders"; + public const string OrderDetail = "/order/"; // usage: /order/{id} + } } diff --git a/src/FrontOffice.Main/Utilities/WalletService.cs b/src/FrontOffice.Main/Utilities/WalletService.cs new file mode 100644 index 0000000..e3c783f --- /dev/null +++ b/src/FrontOffice.Main/Utilities/WalletService.cs @@ -0,0 +1,20 @@ +namespace FrontOffice.Main.Utilities; + +public record WalletBalances(long CreditBalance, long NetworkBalance); +public record WalletTransaction(DateTime Date, long Amount, string Channel, string Description); + +public class WalletService +{ + private WalletBalances _balances = new(350_000, 1_250_000); + private readonly List _transactions = new() + { + new(DateTime.Now.AddDays(-1), 500_000, "درگاه بانکی", "شارژ کیف پول"), + new(DateTime.Now.AddDays(-2), 200_000, "شبکه/معرف", "پاداش شبکه"), + new(DateTime.Now.AddDays(-4), -120_000, "خرید", "برداشت بابت سفارش #1452"), + new(DateTime.Now.AddDays(-9), 900_000, "کیف پول شرکای تجاری", "اعتبار خرید"), + }; + + public Task GetBalancesAsync() => Task.FromResult(_balances); + public Task> GetTransactionsAsync() => Task.FromResult(_transactions.OrderByDescending(t => t.Date).ToList()); +} + diff --git a/src/FrontOffice.Main/wwwroot/css/site.css b/src/FrontOffice.Main/wwwroot/css/site.css index 7d74673..a14fb33 100644 --- a/src/FrontOffice.Main/wwwroot/css/site.css +++ b/src/FrontOffice.Main/wwwroot/css/site.css @@ -366,7 +366,7 @@ html, body { .wizard-card { padding: 1.5rem !important; } -} +} /*#endregion*/ /*#endregion*/ @@ -374,4 +374,29 @@ html, body { #registration-stepper .mud-stepper-content { padding: 0 10px !important; } -} \ No newline at end of file +} + +/*#region Store - Mobile Helpers*/ +@media (max-width: 600px) { + .w-100-mobile { width: 100% !important; } + .qty-input { width: 100% !important; max-width: 220px; } + .mobile-actions-stack { width: 100%; display: flex; gap: .5rem; } + .mobile-actions-stack > * { flex: 1; } +} +/*#endregion*/ + +/*#region Profile Tiles*/ +.tile-link { display: block; color: inherit; } +.tile-link:hover { text-decoration: none; } +.profile-tile { + box-shadow: 0 2px 8px rgba(0,0,0,.06); + transition: box-shadow .2s ease, transform .1s ease; +} +.profile-tile:hover { + box-shadow: 0 4px 16px rgba(0,0,0,.08); + transform: translateY(-2px); +} +.mud-theme-dark .profile-tile { + box-shadow: 0 2px 8px rgba(0,0,0,.25); +} +/*#endregion*/