Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m16s
Build and Deploy to Production / build-and-deploy (push) Successful in 4m16s
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
---
|
||||
# FrontOffice (Blazor Server) staging.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontoffice
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
environment: staging
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontoffice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
containers:
|
||||
- name: frontoffice
|
||||
image: 194.5.195.53:30080/admin/frontoffice:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Staging"
|
||||
- name: GW_URL
|
||||
value: "https://cms.se.kbs1.ir"
|
||||
- name: GwUrl
|
||||
value: "https://cms.se.kbs1.ir"
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontoffice-svc
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
selector:
|
||||
app: frontoffice
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -56,6 +56,7 @@ public static class ConfigureServices
|
||||
services.AddSingleton<UserAuthInfo>();
|
||||
services.AddScoped<AuthService>();
|
||||
services.AddScoped<AuthDialogService>();
|
||||
services.AddScoped<GuestActionGate>();
|
||||
// Storefront services
|
||||
services.AddScoped<CartService>();
|
||||
services.AddScoped<ProductService>();
|
||||
@@ -99,21 +100,36 @@ public static class ConfigureServices
|
||||
|
||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var baseUrl = configuration["GwUrl"];
|
||||
var baseUrl = ResolveGatewayUrl(configuration)
|
||||
?? throw new InvalidOperationException("Gateway URL is missing. Set GW_URL or GwUrl.");
|
||||
|
||||
// Register optimized HttpClient for gRPC
|
||||
var isHttp = baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// When the base URL is plain HTTP (e.g. in-cluster http://cms-svc:8080), we must force HTTP/1.1
|
||||
// so that GrpcChannel does not upgrade to h2c and trigger HTTP_1_1_REQUIRED from Kestrel.
|
||||
// For HTTPS the default negotiation is fine.
|
||||
services.AddScoped(sp =>
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
};
|
||||
HttpMessageHandler inner = isHttp
|
||||
? new SocketsHttpHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
}
|
||||
: new HttpClientHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
};
|
||||
|
||||
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, handler))
|
||||
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, inner))
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(10),
|
||||
BaseAddress = new Uri(baseUrl)
|
||||
BaseAddress = new Uri(baseUrl),
|
||||
DefaultRequestVersion = isHttp ? System.Net.HttpVersion.Version11 : System.Net.HttpVersion.Version20,
|
||||
DefaultVersionPolicy = isHttp
|
||||
? System.Net.Http.HttpVersionPolicy.RequestVersionExact
|
||||
: System.Net.Http.HttpVersionPolicy.RequestVersionOrHigher
|
||||
};
|
||||
});
|
||||
|
||||
@@ -151,12 +167,22 @@ public static class ConfigureServices
|
||||
return services;
|
||||
}
|
||||
|
||||
private static string? ResolveGatewayUrl(IConfiguration configuration)
|
||||
{
|
||||
var envUrl = configuration["GW_URL"];
|
||||
if (!string.IsNullOrWhiteSpace(envUrl))
|
||||
return envUrl.TrimEnd('/');
|
||||
|
||||
return configuration["GwUrl"]?.TrimEnd('/');
|
||||
}
|
||||
|
||||
private static TClient CreateAuthenticatedClient<TClient>(IServiceProvider sp)
|
||||
where TClient : class
|
||||
{
|
||||
var httpClient = sp.GetRequiredService<HttpClient>();
|
||||
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
||||
var baseUrl = httpClient.BaseAddress?.ToString() ?? throw new InvalidOperationException("Base URL not configured");
|
||||
var isHttps = baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
|
||||
{
|
||||
@@ -176,10 +202,14 @@ public static class ConfigureServices
|
||||
}
|
||||
});
|
||||
|
||||
var channelCredentials = isHttps
|
||||
? ChannelCredentials.Create(new SslCredentials(), credentials)
|
||||
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
|
||||
|
||||
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
|
||||
{
|
||||
UnsafeUseInsecureChannelCallCredentials = true,
|
||||
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
|
||||
UnsafeUseInsecureChannelCallCredentials = !isHttps,
|
||||
Credentials = channelCredentials,
|
||||
HttpClient = httpClient,
|
||||
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
|
||||
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
|
||||
|
||||
@@ -20,7 +20,12 @@ FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Trust the staging-ca so server-side gRPC calls to https://cms.se.kbs1.ir succeed without PartialChain
|
||||
COPY ["FrontOffice.Main/staging-ca.crt", "/usr/local/share/ca-certificates/staging-ca.crt"]
|
||||
RUN update-ca-certificates
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:80
|
||||
ENV GW_URL=https://cms.se.kbs1.ir
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["dotnet", "FrontOffice.Main.dll"]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.192" />
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.197" />
|
||||
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
|
||||
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudDivider />
|
||||
|
||||
@@ -7,9 +7,15 @@ public partial class Cart : IDisposable
|
||||
{
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
DiscountCart.OnChange += StateHasChanged;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public partial class Checkout
|
||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
private CustomerAddressModel? _selectedAddress;
|
||||
@@ -32,6 +34,10 @@ public partial class Checkout
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
await VAT.LoadAsync();
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
await LoadAddresses();
|
||||
|
||||
@@ -10,6 +10,7 @@ public partial class ProductDetail
|
||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
|
||||
private DiscountProductDetail? _product;
|
||||
private string _selectedImage = string.Empty;
|
||||
@@ -59,8 +60,14 @@ public partial class ProductDetail
|
||||
_addingToCart = true;
|
||||
try
|
||||
{
|
||||
await DiscountCartService.AddAsync(_product.Id, _quantity);
|
||||
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||
var productId = _product.Id;
|
||||
var qty = _quantity;
|
||||
var title = _product.Title;
|
||||
await GuestGate.RunAsync(async () =>
|
||||
{
|
||||
await DiscountCartService.AddAsync(productId, qty);
|
||||
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
|
||||
private string _search = string.Empty;
|
||||
private long? _selectedCategoryId;
|
||||
@@ -94,7 +95,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
|
||||
private async Task AddToCart(DiscountProductCard p)
|
||||
{
|
||||
await DiscountCart.AddAsync(p.Id);
|
||||
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||
}
|
||||
|
||||
private void NavigateToProduct(long id)
|
||||
|
||||
@@ -93,6 +93,161 @@
|
||||
</section>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
1c. TOP-SELLING REGULAR PRODUCTS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@if (_loadingTopProducts)
|
||||
{
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_topRegularProducts.Any())
|
||||
{
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-6 fade-in-up">
|
||||
<MudText Typo="Typo.h3">پرخریدترین محصولات فروشگاه</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
محبوبترین محصولات کارا بازار سلامت
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||
@foreach (var p in _topRegularProducts)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="4">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||
Style="cursor:pointer;"
|
||||
@onclick="() => NavigateToRegularProduct(p.Id)">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||
</div>
|
||||
}
|
||||
else if (p.RemainingCount <= 5)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;right:8px;">
|
||||
فقط @p.RemainingCount عدد
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||
Disabled="@(p.RemainingCount <= 0)"
|
||||
OnClick="@(() => AddRegularToCart(p))">
|
||||
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
|
||||
Href="@RouteConstants.Store.Products"
|
||||
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||
مشاهده همه محصولات
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@if (_topDiscountProducts.Any())
|
||||
{
|
||||
<section class="section-landing">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-6 fade-in-up">
|
||||
<MudText Typo="Typo.h3">پرخریدترین محصولات فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||
@foreach (var p in _topDiscountProducts)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="4">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||
Style="cursor:pointer;"
|
||||
@onclick="() => NavigateToDiscountProduct(p.Id)">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||
</div>
|
||||
}
|
||||
else if (p.RemainingCount <= 5)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;right:8px;">
|
||||
فقط @p.RemainingCount عدد
|
||||
</MudChip>
|
||||
}
|
||||
@if (p.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;left:8px;">
|
||||
@p.MaxDiscountPercent% اعتبار
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
|
||||
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
|
||||
</div>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||
Disabled="@(p.RemainingCount <= 0)"
|
||||
OnClick="@(() => AddDiscountToCart(p))">
|
||||
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
|
||||
Href="@RouteConstants.DiscountStore.Products"
|
||||
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||
مشاهده همه محصولات اعتباری
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
2. HOW IT WORKS — 3 numbered steps
|
||||
═══════════════════════════════════════════════ *@
|
||||
|
||||
@@ -10,11 +10,22 @@ public partial class Index : IDisposable
|
||||
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private SitePageSettingsService PageSettingsService { get; set; } = default!;
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
// ── CMS page data ──
|
||||
private PageSettingsDto? _pageData;
|
||||
private LandingSettings? _settings;
|
||||
|
||||
// ── Top-selling product sections ──
|
||||
private List<Product> _topRegularProducts = new();
|
||||
private List<DiscountProductCard> _topDiscountProducts = new();
|
||||
private bool _loadingTopProducts = true;
|
||||
|
||||
// ── Latest blog posts (loaded from CMS) ──
|
||||
private List<BlogPostCardDto> _latestPosts = new();
|
||||
|
||||
@@ -51,10 +62,29 @@ public partial class Index : IDisposable
|
||||
PopulateFromSettings();
|
||||
_dataLoaded = true;
|
||||
|
||||
// Load top-selling products and blog posts in parallel
|
||||
var topRegTask = ProductService.GetTopSellingAsync(6);
|
||||
var topDiscTask = DiscountProductService.GetTopSellingAsync(6);
|
||||
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
|
||||
_topRegularProducts = topRegTask.Result.Products;
|
||||
_topDiscountProducts = topDiscTask.Result.Products;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: sections remain empty
|
||||
}
|
||||
_loadingTopProducts = false;
|
||||
|
||||
// Load latest published blog posts
|
||||
try
|
||||
{
|
||||
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
|
||||
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
|
||||
? featuredPostsTask.Result
|
||||
: await BlogPostService.GetFeaturedPostsAsync(2);
|
||||
|
||||
if (_latestPosts.Count < 2)
|
||||
{
|
||||
@@ -267,6 +297,21 @@ public partial class Index : IDisposable
|
||||
Navigation.NavigateTo($"/blog/{slug}");
|
||||
}
|
||||
|
||||
private void NavigateToRegularProduct(long id)
|
||||
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
|
||||
|
||||
private void NavigateToDiscountProduct(long id)
|
||||
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
|
||||
|
||||
private async Task AddRegularToCart(Product p)
|
||||
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||
|
||||
private async Task AddDiscountToCart(DiscountProductCard p)
|
||||
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
|
||||
|
||||
private async void OnStateChanged()
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
@@ -8,11 +8,17 @@ public partial class Cart : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private CartService CartService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
// Navigation and Snackbar are available via _Imports.razor
|
||||
private CartService CartData => CartService;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await CartService.EnsureInitializedAsync();
|
||||
CartService.OnChange += StateHasChanged;
|
||||
|
||||
@@ -14,6 +14,8 @@ public partial class CheckoutSummary : ComponentBase
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
// Snackbar and Navigation are injected via _Imports.razor
|
||||
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
@@ -27,6 +29,10 @@ public partial class CheckoutSummary : ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
await LoadAddresses();
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
|
||||
[Parameter] public long id { get; set; }
|
||||
|
||||
@@ -92,14 +93,18 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
private async Task AddToCart()
|
||||
{
|
||||
if (_product is null) return;
|
||||
await Cart.Add(_product, 1);
|
||||
var product = _product;
|
||||
await GuestGate.RunAsync(() => Cart.Add(product, 1));
|
||||
}
|
||||
|
||||
private async Task RemoveFromCart()
|
||||
{
|
||||
if (_product is null) return;
|
||||
_qty--;
|
||||
await Cart.UpdateQuantity(CurrentCartItem.ProductId, _qty);
|
||||
await GuestGate.RunAsync(async () =>
|
||||
{
|
||||
_qty--;
|
||||
await Cart.UpdateQuantity(CurrentCartItem!.ProductId, _qty);
|
||||
});
|
||||
}
|
||||
|
||||
private void IncreaseLocalQty()
|
||||
|
||||
@@ -21,6 +21,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
|
||||
private string _query = string.Empty;
|
||||
private bool _loading;
|
||||
@@ -112,7 +113,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
|
||||
private async Task AddToCart(Product p)
|
||||
{
|
||||
await Cart.Add(p, 1);
|
||||
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||
}
|
||||
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
|
||||
@@ -80,7 +80,7 @@ public class DiscountProductService
|
||||
public async Task<DiscountProductListResult> GetProductsAsync(
|
||||
int page = 1, int pageSize = 12,
|
||||
string? search = null, long? categoryId = null,
|
||||
bool? inStock = null)
|
||||
bool? inStock = null, string? sortBy = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -97,6 +97,8 @@ public class DiscountProductService
|
||||
request.CategoryId = categoryId.Value;
|
||||
if (inStock.HasValue)
|
||||
request.InStock = inStock.Value;
|
||||
if (!string.IsNullOrWhiteSpace(sortBy))
|
||||
request.SortBy = sortBy;
|
||||
|
||||
var response = await _productClient.GetDiscountProductsAsync(request);
|
||||
|
||||
@@ -125,6 +127,9 @@ public class DiscountProductService
|
||||
}
|
||||
}
|
||||
|
||||
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6)
|
||||
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc");
|
||||
|
||||
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// هر action ای که نیاز به لاگین دارد را از طریق این سرویس اجرا کنید.
|
||||
/// اگر کاربر لاگین نباشد، مودال ورود نشان داده میشود.
|
||||
/// پس از ورود موفق، action به صورت خودکار اجرا میشود.
|
||||
/// </summary>
|
||||
public class GuestActionGate
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private readonly AuthDialogService _authDialogService;
|
||||
|
||||
public GuestActionGate(AuthService authService, AuthDialogService authDialogService)
|
||||
{
|
||||
_authService = authService;
|
||||
_authDialogService = authDialogService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اگر کاربر لاگین باشد action را اجرا میکند.
|
||||
/// در غیر این صورت مودال لاگین را باز کرده و پس از ورود موفق، action را اجرا میکند.
|
||||
/// </summary>
|
||||
/// <returns>true اگر action اجرا شد، false اگر کاربر لاگین نکرد.</returns>
|
||||
public async Task<bool> RunAsync(Func<Task> action)
|
||||
{
|
||||
if (await _authService.IsAuthenticatedAsync())
|
||||
{
|
||||
await action();
|
||||
return true;
|
||||
}
|
||||
|
||||
await _authDialogService.ShowAuthDialogAsync();
|
||||
|
||||
if (await _authService.IsAuthenticatedAsync())
|
||||
{
|
||||
await action();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,9 @@ public class ProductService
|
||||
return result.Products;
|
||||
}
|
||||
|
||||
public Task<ProductListResult> GetTopSellingAsync(int count = 6)
|
||||
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count);
|
||||
|
||||
public async Task<ProductListResult> GetProductsPagedAsync(
|
||||
string? query = null, long? categoryId = null, string? sortBy = null,
|
||||
int page = 1, int pageSize = 12)
|
||||
|
||||
@@ -69,7 +69,7 @@ public class TokenNotificationService : IAsyncDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var gwUrl = _configuration["GwUrl"]?.TrimEnd('/') ?? "https://localhost:5002";
|
||||
var gwUrl = (_configuration["GW_URL"] ?? _configuration["GwUrl"])?.TrimEnd('/') ?? "https://localhost:5002";
|
||||
var hubPath = _configuration["SignalR:HubPath"] ?? "/hubs/token-relay";
|
||||
var hubUrl = $"{gwUrl}{hubPath}";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"GwUrl": "https://localhost:32846",
|
||||
"GwUrl": "https://cms.se.kbs1.ir",
|
||||
"DownloadUrl": "",
|
||||
"EncryptionSettings": {
|
||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC9jCCAd6gAwIBAgIQAwobM2pDCheGeQdvVZzMPTANBgkqhkiG9w0BAQsFADAV
|
||||
MRMwEQYDVQQDEwpzdGFnaW5nLWNhMB4XDTI2MDQyODE5MjU0NVoXDTM2MDQyNTE5
|
||||
MjU0NVowFTETMBEGA1UEAxMKc3RhZ2luZy1jYTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBALOOLPxgHegrkI9YXm/0wHKchE5ukb8omv2oDYPp/CjQn8yJ
|
||||
KBpn+8tev8wT1SECACNuA3XhxtjV9dryV5U6lxkmQv2YlLrJOFa3ljcODQpAkKXl
|
||||
Q+PMWic2VyO9/UkW1a4HcPQfGhpgN710evOfBFB4Ora4CUVsADKPLbjJfX8jpqkU
|
||||
LAZiQCrA1kJ087fiKheuXEAWPgcwEE5q0BCs876zIHyST6FaLVNabM5/m0sr7Bky
|
||||
1GQCJlgOLtUeDkaXAv/WJqq/LetcjNy2dWS26PJc7C2byShTfaaWDM8Ki5FG/uMQ
|
||||
/6L54Lx5QKGbl8MzIKmEscXe4GYnmE8MyHQX0aECAwEAAaNCMEAwDgYDVR0PAQH/
|
||||
BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFELZSJbeWDRQgj6cTQWY
|
||||
K2sT9ZsLMA0GCSqGSIb3DQEBCwUAA4IBAQCT2eGmHcd1t4K80NobrUzm1d3SB4EW
|
||||
XOLCR+TTYFIYsim0eltwzrOuch+MBTf4+v+4zXWE4e7d1B1lzJIqFIXcgLpDu0i2
|
||||
QeZMrPxY1rkUhqGPwwsoOWBIK/cGot2yg6SZEwj64f7lKUbUB8aWY+xSzct2mwlG
|
||||
i0j6wt0/eRh6hKGdvEiYORffdubC94utkNzr0kFszY4yb3vG0zCVWYiGzkxe0IiF
|
||||
Gc8zN8ZHTY9tk+G5wQuj2WAo7M3DXgFWBbhNgharKulEXTxLXhQksw/nXITa2IwM
|
||||
qt4MWHdGn7/iIyUp/774ySkFk2n6cHi7gQAvolT6OZNCuAOYm8+No7P6
|
||||
-----END CERTIFICATE-----
|
||||
Reference in New Issue
Block a user