37c7cebf92
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 23m11s
- Modified `GetTopSellingAsync` methods in `ProductService` and `DiscountProductService` to include an optional `inStock` parameter for filtering products based on availability. - Introduced a constant for the top product count in `Index.razor.cs` to improve maintainability and readability. - Updated the product retrieval logic in `Index.razor.cs` to utilize the new in-stock filtering feature. These changes enhance the product listing functionality by allowing users to view only in-stock top-selling products, improving the overall shopping experience.
363 lines
15 KiB
C#
363 lines
15 KiB
C#
using FrontOffice.Main.Utilities;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.JSInterop;
|
|
using MudBlazor;
|
|
|
|
namespace FrontOffice.Main.Pages;
|
|
|
|
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!;
|
|
|
|
private bool _isAuthenticated;
|
|
|
|
// ── CMS page data ──
|
|
private PageSettingsDto? _pageData;
|
|
private LandingSettings? _settings;
|
|
|
|
// ── Top-selling product sections ──
|
|
private const int LandingTopProductCount = 6;
|
|
|
|
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();
|
|
|
|
// ── Data lists (populated from DB or fallback) ──
|
|
private List<(string Icon, string Text)> _trustBadges = new();
|
|
private List<(string Title, string Desc)> _steps = new();
|
|
private List<(string Icon, string Title, string Desc, int Delay)> _features = new();
|
|
private List<StatItem> _stats = new();
|
|
private List<TestimonialItem> _testimonials = new();
|
|
private List<QA> _faqs = new();
|
|
|
|
// Track whether animations need re-initialization after data load
|
|
private bool _dataLoaded;
|
|
private bool _animationsInitialized;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
MainService.OnChangeHandler += OnStateChanged;
|
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
|
|
|
// Load landing page settings from CMS
|
|
try
|
|
{
|
|
_pageData = await PageSettingsService.GetPageAsync("landing");
|
|
if (_pageData != null)
|
|
{
|
|
_settings = _pageData.GetSettings<LandingSettings>();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Fallback: settings remain null → defaults below
|
|
}
|
|
|
|
PopulateFromSettings();
|
|
_dataLoaded = true;
|
|
|
|
// Load top-selling in-stock products and blog posts in parallel
|
|
var topRegTask = ProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
|
var topDiscTask = DiscountProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
|
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 = featuredPostsTask.IsCompletedSuccessfully
|
|
? featuredPostsTask.Result
|
|
: await BlogPostService.GetFeaturedPostsAsync(2);
|
|
|
|
if (_latestPosts.Count < 2)
|
|
{
|
|
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
|
|
var existing = _latestPosts.Select(p => p.Id).ToHashSet();
|
|
foreach (var post in result.Posts)
|
|
{
|
|
if (!existing.Contains(post.Id))
|
|
{
|
|
_latestPosts.Add(post);
|
|
if (_latestPosts.Count >= 2) break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_latestPosts = new();
|
|
}
|
|
}
|
|
|
|
private void PopulateFromSettings()
|
|
{
|
|
// ── Trust badges ──
|
|
if (_settings?.TrustBadges?.Any() == true)
|
|
{
|
|
_trustBadges = _settings.TrustBadges
|
|
.Select(b => (ResolveIcon(b.IconName), b.Text ?? ""))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_trustBadges = new()
|
|
{
|
|
(Icons.Material.Outlined.Timer, "ثبتنام زیر ۲ دقیقه"),
|
|
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
|
|
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
|
|
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
|
|
};
|
|
}
|
|
|
|
// ── Steps ──
|
|
if (_settings?.Steps?.Any() == true)
|
|
{
|
|
_steps = _settings.Steps
|
|
.Select(s => (s.Title ?? "", s.Description ?? ""))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_steps = new()
|
|
{
|
|
("ثبتنام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
|
|
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
|
|
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظهای دریافت کنید."),
|
|
};
|
|
}
|
|
|
|
// ── Features ──
|
|
if (_settings?.Features?.Any() == true)
|
|
{
|
|
_features = _settings.Features
|
|
.Select((f, i) => (ResolveIcon(f.IconName), f.Title ?? "", f.Description ?? "", i * 100))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_features = new()
|
|
{
|
|
(Icons.Material.Outlined.VerifiedUser, "ثبتنام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
|
|
(Icons.Material.Outlined.StarBorder, "پاداشهای شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارشها.", 100),
|
|
(Icons.Material.Outlined.Devices, "طراحی واکنشگرا", "تجربهای روان در موبایل و دسکتاپ.", 200),
|
|
(Icons.Material.Outlined.Groups, "تیمسازی هوشمند", "ساختار درختی شبکه و مدیریت تیمهای فروش.", 300),
|
|
(Icons.Material.Outlined.Insights, "گزارشهای لحظهای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
|
|
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحلهای.", 500),
|
|
};
|
|
}
|
|
|
|
// ── Stats ──
|
|
if (_settings?.Stats?.Any() == true)
|
|
{
|
|
_stats = _settings.Stats
|
|
.Select((s, i) => new StatItem(
|
|
$"stat-{i}",
|
|
$"{s.Value}{s.Suffix}",
|
|
s.Value,
|
|
s.Suffix ?? "",
|
|
ResolveColor(s.Color),
|
|
s.Label ?? ""))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_stats = new()
|
|
{
|
|
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
|
|
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپتایم سرویس"),
|
|
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
|
|
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
|
|
};
|
|
}
|
|
|
|
// ── Testimonials ──
|
|
if (_settings?.Testimonials?.Any() == true)
|
|
{
|
|
_testimonials = _settings.Testimonials
|
|
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_testimonials = new()
|
|
{
|
|
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیمها بدون اکسل و دردسر انجام میشود.", "شرکت سینا نت", "مدیر عملیات", 0),
|
|
new("سازمان فروش بصری و گزارشهای دقیق باعث شد رشد تیم را لحظهای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
|
|
new("سادگی ثبتنام و شفافیت پاداشها مهمترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
|
|
};
|
|
}
|
|
|
|
// ── FAQs ──
|
|
if (_settings?.Faqs?.Any() == true)
|
|
{
|
|
_faqs = _settings.Faqs
|
|
.Select(f => new QA(f.Question ?? "", f.Answer ?? ""))
|
|
.ToList();
|
|
}
|
|
else
|
|
{
|
|
_faqs = new()
|
|
{
|
|
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر میشود."),
|
|
new("با دیتابیس خودم کار میکند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی میشود."),
|
|
new("چه درگاههایی پشتیبانی میشود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وبهوکها."),
|
|
new("میتوانم دادهها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve icon name from SettingsJson to MudBlazor icon string.
|
|
/// Falls back to a generic icon.
|
|
/// </summary>
|
|
private static string ResolveIcon(string? iconName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(iconName)) return Icons.Material.Outlined.Info;
|
|
|
|
// Try to get from MudBlazor Icons via reflection
|
|
var field = typeof(Icons.Material.Outlined).GetField(iconName,
|
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
|
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
|
|
|
|
// Also try Filled
|
|
field = typeof(Icons.Material.Filled).GetField(iconName,
|
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
|
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
|
|
|
|
return Icons.Material.Outlined.Info;
|
|
}
|
|
|
|
private static Color ResolveColor(string? colorName) => colorName?.ToLowerInvariant() switch
|
|
{
|
|
"primary" => Color.Primary,
|
|
"secondary" => Color.Secondary,
|
|
"success" => Color.Success,
|
|
"warning" => Color.Warning,
|
|
"error" => Color.Error,
|
|
"info" => Color.Info,
|
|
_ => Color.Default
|
|
};
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
// Init/re-init scroll animations after data is loaded and rendered
|
|
if ((firstRender || (_dataLoaded && !_animationsInitialized)) && _steps.Any())
|
|
{
|
|
_animationsInitialized = true;
|
|
|
|
// Init scroll-triggered fade-in animations
|
|
await JS.InvokeVoidAsync("initScrollAnimations");
|
|
|
|
// Animate stat counters
|
|
foreach (var stat in _stats)
|
|
{
|
|
await JS.InvokeVoidAsync("animateCounter", stat.ElementId, stat.Target, 2200, stat.Suffix);
|
|
}
|
|
}
|
|
|
|
if (await AuthService.IsAuthenticatedAsync())
|
|
{
|
|
if (await AuthService.IsCompleteRegisterAsync())
|
|
{
|
|
Navigation.NavigateTo(RouteConstants.Profile.Index);
|
|
}
|
|
else
|
|
{
|
|
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
|
|
}
|
|
}
|
|
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
}
|
|
|
|
private void NavigateToRegistrationWizard()
|
|
{
|
|
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
|
|
}
|
|
|
|
private void NavigateToPost(string slug)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// ── Records ──
|
|
private record QA(string Q, string A);
|
|
private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label);
|
|
private record TestimonialItem(string Quote, string Name, string Role, int Delay);
|
|
|
|
// ── SettingsJson DTO ──
|
|
private class LandingSettings
|
|
{
|
|
public string? HeroButtonPrimaryText { get; set; }
|
|
public string? HeroButtonSecondaryText { get; set; }
|
|
public List<TrustBadgeItem>? TrustBadges { get; set; }
|
|
public string? StepsTitle { get; set; }
|
|
public List<StepItem>? Steps { get; set; }
|
|
public string? FeaturesTitle { get; set; }
|
|
public List<FeatureItem>? Features { get; set; }
|
|
public string? StatsTitle { get; set; }
|
|
public List<StatSettingItem>? Stats { get; set; }
|
|
public string? TestimonialsTitle { get; set; }
|
|
public List<TestimonialSettingItem>? Testimonials { get; set; }
|
|
public string? FaqTitle { get; set; }
|
|
public List<FaqItem>? Faqs { get; set; }
|
|
public string? CtaTitle { get; set; }
|
|
public string? CtaDescription { get; set; }
|
|
public string? CtaButtonText { get; set; }
|
|
public bool? FeaturedBlogEnabled { get; set; }
|
|
}
|
|
|
|
private class TrustBadgeItem { public string? IconName { get; set; } public string? Text { get; set; } }
|
|
private class StepItem { public string? Title { get; set; } public string? Description { get; set; } }
|
|
private class FeatureItem { public string? IconName { get; set; } public string? Title { get; set; } public string? Description { get; set; } }
|
|
private class StatSettingItem { public string? Label { get; set; } public double Value { get; set; } public string? Suffix { get; set; } public string? Color { get; set; } }
|
|
private class TestimonialSettingItem { public string? Quote { get; set; } public string? Name { get; set; } public string? Role { get; set; } }
|
|
private class FaqItem { public string? Question { get; set; } public string? Answer { get; set; } }
|
|
|
|
public void Dispose()
|
|
{
|
|
MainService.OnChangeHandler -= OnStateChanged;
|
|
}
|
|
} |