merge: kub-stage → production (1:1 square images + site pages + landing animation fix)
Build and Deploy to Production / build-and-deploy (push) Has been cancelled

This commit is contained in:
masoodafar-web
2026-02-18 01:01:44 +03:30
25 changed files with 578 additions and 214 deletions
@@ -22,6 +22,7 @@ using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
@@ -83,6 +84,8 @@ public static class ConfigureServices
services.AddScoped<DiscountProductService>();
services.AddScoped<DiscountCartService>();
services.AddScoped<DiscountOrderService>();
// Site Page Settings Service (simplified page management)
services.AddScoped<SitePageSettingsService>();
// Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only)
@@ -133,6 +136,7 @@ public static class ConfigureServices
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Commission.CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.AppVersion.AppVersionContract.AppVersionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient>);
services.AddScoped(CreateAuthenticatedClient<SitePageSettingsContract.SitePageSettingsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient>);
// Image Resolver Service
+1 -1
View File
@@ -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.179" />
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.180" />
<!--<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
+18 -36
View File
@@ -1,6 +1,6 @@
@attribute [Route(RouteConstants.About.Index)]
@inject NavigationManager Navigation
@inject SitePageService SitePageService
@inject SitePageSettingsService PageSettingsService
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
@@ -66,40 +66,22 @@ else
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">
@(_visionSection?.Title ?? "چشم‌انداز")
@(_settings?.VisionTitle ?? "چشم‌انداز")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.VisionText ?? "تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.")
</MudText>
@if (!string.IsNullOrWhiteSpace(_visionSection?.HtmlContent))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@((MarkupString)_visionSection.HtmlContent)
</MudText>
}
else
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">
تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
</MudText>
}
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">
@(_missionSection?.Title ?? "مأموریت")
@(_settings?.MissionTitle ?? "مأموریت")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.MissionText ?? "توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد فرصت‌های جدید برای رشد و توسعه پایدار.")
</MudText>
@if (!string.IsNullOrWhiteSpace(_missionSection?.HtmlContent))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@((MarkupString)_missionSection.HtmlContent)
</MudText>
}
else
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">
توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار.
</MudText>
}
</MudPaper>
</MudItem>
</MudGrid>
@@ -111,21 +93,21 @@ else
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@if (_valueSections.Any())
@if (_valueImages.Any())
{
@foreach (var value in _valueSections)
@foreach (var value in _valueImages)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
@if (!string.IsNullOrWhiteSpace(value.IconName))
{
<MudIcon Icon="@value.IconName" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudIcon Icon="@($"Icons.Material.Filled.{value.IconName}")" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
}
<MudText Typo="Typo.h6" Class="mb-2">@value.Title</MudText>
@if (!string.IsNullOrWhiteSpace(value.HtmlContent))
@if (!string.IsNullOrWhiteSpace(value.Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@((MarkupString)value.HtmlContent)
@value.Description
</MudText>
}
</MudPaper>
@@ -199,9 +181,9 @@ else
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
@if (_teamSections.Any())
@if (_teamImages.Any())
{
@foreach (var member in _teamSections)
@foreach (var member in _teamImages)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
@@ -222,10 +204,10 @@ else
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@member.Subtitle</MudText>
}
@if (!string.IsNullOrWhiteSpace(member.HtmlContent))
@if (!string.IsNullOrWhiteSpace(member.Description))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@((MarkupString)member.HtmlContent)
@member.Description
</MudText>
}
</MudPaper>
+20 -10
View File
@@ -5,24 +5,22 @@ namespace FrontOffice.Main.Pages;
public partial class About
{
private bool _loading = true;
private SitePageDto? _page;
private SitePageSectionDto? _visionSection;
private SitePageSectionDto? _missionSection;
private List<SitePageSectionDto> _valueSections = new();
private List<SitePageSectionDto> _teamSections = new();
private PageSettingsDto? _page;
private AboutSettings? _settings;
private List<PageSettingsImageDto> _valueImages = new();
private List<PageSettingsImageDto> _teamImages = new();
protected override async Task OnInitializedAsync()
{
try
{
_page = await SitePageService.GetByKeyAsync("about");
_page = await PageSettingsService.GetPageAsync("about");
if (_page != null)
{
_visionSection = _page.GetSection("vision");
_missionSection = _page.GetSection("mission");
_valueSections = _page.GetSections("value-").ToList();
_teamSections = _page.GetSections("team-").ToList();
_settings = _page.GetSettings<AboutSettings>();
_valueImages = _page.GetImages("values");
_teamImages = _page.GetImages("team");
}
}
catch
@@ -34,4 +32,16 @@ public partial class About
_loading = false;
}
}
private class AboutSettings
{
public string? VisionTitle { get; set; }
public string? VisionText { get; set; }
public string? VisionIcon { get; set; }
public string? MissionTitle { get; set; }
public string? MissionText { get; set; }
public string? MissionIcon { get; set; }
public string? ValuesTitle { get; set; }
public string? TeamTitle { get; set; }
}
}
+10 -10
View File
@@ -1,5 +1,5 @@
@attribute [Route(RouteConstants.Contact.Index)]
@inject SitePageService SitePageService
@inject SitePageSettingsService PageSettingsService
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
@@ -133,7 +133,7 @@
<div>
<MudText Typo="Typo.body2" >آدرس</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</div>
</MudStack>
@@ -142,7 +142,7 @@
<MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >تلفن</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactPhone ?? "026-34233563")</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Phone ?? "026-34233563")</MudText>
</div>
</MudStack>
@@ -150,7 +150,7 @@
<MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >ایمیل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactEmail ?? "info@kbs1.co")</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Email ?? "info@kbs1.co")</MudText>
</div>
</MudStack>
@@ -159,7 +159,7 @@
<div>
<MudText Typo="Typo.body2" >ساعات کاری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@((MarkupString)(_contactHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
@((MarkupString)(_settings?.WorkingHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
</MudText>
</div>
</MudStack>
@@ -174,7 +174,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Telegram"
Href="@(_socialTelegram ?? "https://t.me/kbs1")"
Href="@(_settings?.TelegramUrl ?? "https://t.me/kbs1")"
Target="_blank"
FullWidth="true">
تلگرام
@@ -183,7 +183,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Instagram"
Href="@(_socialInstagram ?? "https://instagram.com/kbs1")"
Href="@(_settings?.InstagramUrl ?? "https://instagram.com/kbs1")"
Target="_blank"
FullWidth="true">
اینستاگرام
@@ -192,7 +192,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.LinkedIn"
Href="@(_socialLinkedin ?? "https://linkedin.com/company/kbs1")"
Href="@(_settings?.LinkedinUrl ?? "https://linkedin.com/company/kbs1")"
Target="_blank"
FullWidth="true">
لینکدین
@@ -201,7 +201,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.WhatsApp"
Href="@(_socialWhatsapp ?? "https://wa.me/989123456789")"
Href="@(_settings?.WhatsappUrl ?? "https://wa.me/989123456789")"
Target="_blank"
FullWidth="true">
واتس‌اپ
@@ -226,7 +226,7 @@
<MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Large" Style="color: white;" />
<MudText Typo="Typo.h6" Style="color: white;">نقشه موقعیت مکانی</MudText>
<MudText Typo="Typo.body2" Style="color: rgba(255,255,255,0.8);" Align="Align.Center">
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</MudStack>
</div>
+14 -51
View File
@@ -12,52 +12,19 @@ public partial class Contact
private bool _isSubmitting;
private readonly ContactFormValidator _contactFormValidator = new();
// Dynamic CMS data
private SitePageDto? _page;
private string? _contactAddress;
private string? _contactPhone;
private string? _contactEmail;
private string? _contactHours;
private string? _socialTelegram;
private string? _socialInstagram;
private string? _socialLinkedin;
private string? _socialWhatsapp;
// Dynamic CMS data (new simplified system)
private PageSettingsDto? _page;
private ContactSettings? _settings;
protected override async Task OnInitializedAsync()
{
try
{
_page = await SitePageService.GetByKeyAsync("contact");
_page = await PageSettingsService.GetPageAsync("contact");
if (_page != null)
{
// Load contact info from CMS section
var contactInfoSection = _page.GetSection("contact-info");
if (contactInfoSection != null)
{
var contactData = contactInfoSection.GetExtraData<ContactInfoData>();
if (contactData != null)
{
_contactAddress = contactData.Address;
_contactPhone = contactData.Phone;
_contactEmail = contactData.Email;
_contactHours = contactData.Hours;
}
}
// Load social media links from CMS section
var socialSection = _page.GetSection("social-media");
if (socialSection != null)
{
var socialData = socialSection.GetExtraData<SocialMediaData>();
if (socialData != null)
{
_socialTelegram = socialData.Telegram;
_socialInstagram = socialData.Instagram;
_socialLinkedin = socialData.Linkedin;
_socialWhatsapp = socialData.Whatsapp;
}
}
_settings = _page.GetSettings<ContactSettings>();
}
}
catch
@@ -111,12 +78,12 @@ public partial class Contact
private void CallSupport()
{
Snackbar.Add($"شماره تماس: {_contactPhone ?? "026-34233563"}", Severity.Info);
Snackbar.Add($"شماره تماس: {_settings?.Phone ?? "026-34233563"}", Severity.Info);
}
private void SendEmail()
{
Snackbar.Add($"ایمیل: {_contactEmail ?? "info@kbs1.co"}", Severity.Info);
Snackbar.Add($"ایمیل: {_settings?.Email ?? "info@kbs1.co"}", Severity.Info);
}
public class ContactForm
@@ -142,20 +109,16 @@ public partial class Contact
}
}
// DTO classes for ExtraData JSON deserialization
private class ContactInfoData
// DTO class for SettingsJson deserialization
private class ContactSettings
{
public string? Address { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Hours { get; set; }
}
private class SocialMediaData
{
public string? Telegram { get; set; }
public string? Instagram { get; set; }
public string? Linkedin { get; set; }
public string? Whatsapp { get; set; }
public string? WorkingHours { get; set; }
public string? TelegramUrl { get; set; }
public string? InstagramUrl { get; set; }
public string? LinkedinUrl { get; set; }
public string? WhatsappUrl { get; set; }
}
}
@@ -33,7 +33,7 @@
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
@@ -82,7 +82,7 @@
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
@@ -80,7 +80,7 @@
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="40" ImgHeight="40" Class="rounded-circle" />
ImgWidth="40" ImgHeight="40" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
@@ -24,8 +24,8 @@
<MudItem xs="12" md="5">
<MudPaper Class="pa-2 rounded-xl" Elevation="1">
<AppImage Path="@_selectedImage" Alt="@_product.Title"
ObjectFit="ObjectFit.Contain"
Style="width:100%; max-height:400px; border-radius:12px;" />
ObjectFit="ObjectFit.Cover"
Style="width:100%; aspect-ratio:1/1; border-radius:12px;" />
@if (_product.Images.Count > 1)
{
@@ -77,9 +77,9 @@
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;height: 300px">
Style="cursor:pointer;">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="height: 60%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
<div style="aspect-ratio:1/1;width:100%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : 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;">
+11 -11
View File
@@ -15,11 +15,11 @@
</MudChip>
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
رشد تیم، فروش واقعی، پاداش شفاف
@(_pageData?.HeroTitle ?? "رشد تیم، فروش واقعی، پاداش شفاف")
</MudText>
<MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.
@(_pageData?.HeroSubtitle ?? "با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.")
</MudText>
<MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
@@ -27,13 +27,13 @@
Class="rounded-pill hero-cta-primary"
Size="Size.Large"
OnClick="NavigateToRegistrationWizard">
شروع رایگان
@(_settings?.HeroButtonPrimaryText ?? "شروع رایگان")
</MudButton>
<MudButton Variant="Variant.Outlined"
Class="rounded-pill hero-cta-outline"
Size="Size.Large"
OnClick="@(() => Navigation.NavigateTo("/blog"))">
آخرین اخبار
@(_settings?.HeroButtonSecondaryText ?? "آخرین اخبار")
</MudButton>
</MudStack>
@@ -99,7 +99,7 @@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">سه گام تا شروع</MudText>
<MudText Typo="Typo.h3">@(_settings?.StepsTitle ?? "سه گام تا شروع")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بدون پیچیدگی، سریع و آسان
</MudText>
@@ -128,7 +128,7 @@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">چرا کارا بازار سلامت؟</MudText>
<MudText Typo="Typo.h3">@(_settings?.FeaturesTitle ?? "چرا کارا بازار سلامت؟")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2" Style="max-width:540px;margin:0 auto;">
ابزارهایی ساده و قدرتمند برای رشد کسب‌وکار شما
</MudText>
@@ -259,7 +259,7 @@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">اعتماد مشتریان</MudText>
<MudText Typo="Typo.h3">@(_settings?.TestimonialsTitle ?? "اعتماد مشتریان")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بخشی از تجربه استفاده از «کارا بازار سلامت»
</MudText>
@@ -299,7 +299,7 @@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Medium">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">سوالات متداول</MudText>
<MudText Typo="Typo.h3">@(_settings?.FaqTitle ?? "سوالات متداول")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
پاسخ به سوالات رایج شما
</MudText>
@@ -326,16 +326,16 @@
<div class="cta-banner pa-8 pa-md-12 fade-in-up">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h3" Align="Align.Center" Class="dash-hero-name">
آماده شروع هستید؟
@(_settings?.CtaTitle ?? "آماده شروع هستید؟")
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.
@(_settings?.CtaDescription ?? "همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.")
</MudText>
<MudButton Variant="Variant.Filled"
Size="Size.Large"
Class="rounded-pill mt-2 hero-cta-primary"
OnClick="NavigateToRegistrationWizard">
شروع رایگان
@(_settings?.CtaButtonText ?? "شروع رایگان")
</MudButton>
</MudStack>
</div>
+217 -58
View File
@@ -9,21 +9,53 @@ 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!;
// ── CMS page data ──
private PageSettingsDto? _pageData;
private LandingSettings? _settings;
// ── 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;
// 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 latest published blog posts
// اول پین‌شده‌ها، اگر کافی نبود آخرین پست‌ها اضافه شود
try
{
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
// اگر فقط ۱ پین‌شده داریم، ۱ پست آخر هم اضافه کن
if (_latestPosts.Count < 2)
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
@@ -44,10 +76,162 @@ public partial class Index : IDisposable
}
}
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)
{
if (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");
@@ -88,65 +272,40 @@ public partial class Index : IDisposable
await InvokeAsync(StateHasChanged);
}
// ── Trust badges ──
private readonly List<(string Icon, string Text)> _trustBadges = new()
{
(Icons.Material.Outlined.Timer, "ثبت‌نام زیر ۲ دقیقه"),
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
};
// ── How it works steps ──
private readonly List<(string Title, string Desc)> _steps = new()
{
("ثبت‌نام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظه‌ای دریافت کنید."),
};
// ── Feature cards data (6 cards) ──
private readonly List<(string Icon, string Title, string Desc, int Delay)> _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 data (animated counters) ──
private readonly List<StatItem> _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 ──
private readonly List<TestimonialItem> _testimonials = new()
{
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.", "شرکت سینا نت", "مدیر عملیات", 0),
new("شجره‌نامه بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
new("سادگی ثبت‌نام و شفافیت پاداش‌ها مهم‌ترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
};
// ── FAQ data ──
private readonly List<QA> _faqs = new()
{
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
};
// ── 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;
+99
View File
@@ -0,0 +1,99 @@
@attribute [Route(RouteConstants.Licenses.Index)]
@inject SitePageSettingsService PageSettingsService
<PageTitle>مجوزها و گواهینامه‌ها | کارا بازار سلامت</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
}
else
{
<!-- Hero Section -->
<section class="licenses-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled">مجوزها و گواهینامه‌ها</MudChip>
<MudText Typo="Typo.h2" Class="mb-3">
@(_pageData?.HeroTitle ?? "مجوزها و گواهینامه‌های رسمی")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 700px;">
@(_pageData?.HeroSubtitle ?? "ما با دریافت مجوزهای رسمی و تأییدیه‌های معتبر، اطمینان شما را در استفاده از خدمات خود جلب کرده‌ایم.")
</MudText>
</MudStack>
</MudContainer>
</section>
<MudStack Spacing="8" Class="pb-12">
<!-- Licenses Gallery -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">مجوزهای ما</MudText>
@if (_licenseImages.Any())
{
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var license in _licenseImages)
{
<MudItem xs="12" sm="6" md="4">
<MudCard Elevation="2" Class="h-100">
<MudCardMedia Image="@license.ImagePath" Height="250" />
<MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.Title))
{
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-1">@license.Title</MudText>
}
@if (!string.IsNullOrWhiteSpace(license.Description))
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">@license.Description</MudText>
}
</MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.LinkUrl))
{
<MudCardActions Class="justify-center">
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Href="@license.LinkUrl"
Target="_blank"
StartIcon="@Icons.Material.Filled.OpenInNew">
مشاهده جزئیات
</MudButton>
</MudCardActions>
}
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Class="my-8" Dense="true">
اطلاعات مجوزها به‌زودی بارگذاری خواهد شد.
</MudAlert>
}
</MudContainer>
</section>
<!-- CTA Section -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="3" Class="pa-8 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h4" Class="mb-3">نیاز به اطلاعات بیشتر دارید؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
برای دریافت اطلاعات بیشتر درباره مجوزها و گواهینامه‌های ما، با ما تماس بگیرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudPaper>
</MudContainer>
</section>
</MudStack>
}
@@ -0,0 +1,31 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class Licenses
{
private bool _loading = true;
private PageSettingsDto? _pageData;
private List<PageSettingsImageDto> _licenseImages = new();
protected override async Task OnInitializedAsync()
{
try
{
_pageData = await PageSettingsService.GetPageAsync("licenses");
if (_pageData != null)
{
_licenseImages = _pageData.GetImages("licenses");
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
}
+2 -2
View File
@@ -31,7 +31,7 @@
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@context.Title</MudText>
</MudStack>
@if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description))
@@ -87,7 +87,7 @@
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
@if (item.Discount > 0)
@@ -86,7 +86,7 @@
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="48" ImgHeight="48" Class="rounded-circle" />
ImgWidth="48" ImgHeight="48" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText>
</MudStack>
@@ -47,7 +47,7 @@ else
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@context.ProductTitle</MudText>
</MudStack>
</MudTd>
@@ -66,7 +66,7 @@ else
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@it.ProductTitle</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -33,7 +33,7 @@ else
{
<AppImage Path="@MainImageUrl" Alt="@MainImageAlt"
ObjectFit="ObjectFit.Cover"
Style="width:100%; max-height:400px; border-radius:12px;" />
Style="width:100%; aspect-ratio:1/1; border-radius:12px;" />
}
else
{
@@ -120,11 +120,11 @@
<MudItem xs="6" sm="6" md="3"
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;height: 300px">
Style="cursor:pointer;">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="height: 60%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
<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;">
+1
View File
@@ -55,6 +55,7 @@
<MudLink Href="@(RouteConstants.FAQ.Index)" Class="footer-link">سوالات متداول</MudLink>
<MudLink Href="@(RouteConstants.Contact.Index)" Class="footer-link">ارتباط با ما</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Class="footer-link">درباره ما</MudLink>
<MudLink Href="@(RouteConstants.Licenses.Index)" Class="footer-link">مجوزها</MudLink>
</MudStack>
</MudItem>
+7 -3
View File
@@ -19,7 +19,7 @@
</MudHidden>
<div class="d-flex align-center gap-2">
<MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None">
<MudLink Href="@(_isAuthenticated ? RouteConstants.Profile.Index : RouteConstants.Main.MainPage)" Underline="Underline.None">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudImage ObjectFit="ObjectFit.Cover"
@@ -73,6 +73,7 @@
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" Class="mud-link">بلاگ</MudLink>
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" Class="mud-link">ارتباط با ما</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" Class="mud-link">درباره ما</MudLink>
<MudLink Href="@(RouteConstants.Licenses.Index)" Typo="Typo.subtitle1" Class="mud-link">مجوزها</MudLink>
</div>
<div class="d-flex align-center gap-2">
@@ -191,6 +192,9 @@
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
ارتباط با ما
</MudLink>
<MudLink Href="@(RouteConstants.Licenses.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
مجوزها
</MudLink>
<MudDivider Class="my-2"/>
@if (_isAuthenticated)
@@ -281,9 +285,9 @@
else
{
@* ── Default bottom nav (outside stores) ── *@
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudLink Href="@(_isAuthenticated ? RouteConstants.Profile.Index : RouteConstants.Main.MainPage)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">خانه</MudText>
<MudText Typo="Typo.caption">@(_isAuthenticated ? "داشبورد" : "خانه")</MudText>
</MudLink>
@if (_isAuthenticated)
{
@@ -67,6 +67,11 @@ public static class RouteConstants
public const string Index = "/contact";
}
public static class Licenses
{
public const string Index = "/licenses";
}
public static class Blog
{
public const string Index = "/blog";
@@ -0,0 +1,125 @@
using System.Text.Json;
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
namespace FrontOffice.Main.Utilities;
/// <summary>
/// سرویس ساده‌شده مدیریت صفحات سایت
/// ۴ صفحه ثابت: landing, about, contact, licenses
/// </summary>
public class SitePageSettingsService
{
private readonly SitePageSettingsContract.SitePageSettingsContractClient _client;
public SitePageSettingsService(SitePageSettingsContract.SitePageSettingsContractClient client)
{
_client = client;
}
/// <summary>
/// دریافت تنظیمات صفحه با کلید
/// </summary>
public async Task<PageSettingsDto?> GetPageAsync(string pageKey)
{
try
{
var response = await _client.GetPageSettingsAsync(
new GetPageSettingsRequest { PageKey = pageKey });
if (response == null || response.Id <= 0) return null;
return MapToDto(response);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"SitePageSettingsService.GetPageAsync error: {ex.Message}");
#endif
return null;
}
}
private static PageSettingsDto MapToDto(PageSettingsResponse response) => new()
{
Id = response.Id,
PageKey = response.PageKey,
Title = response.Title,
MetaDescription = response.MetaDescription,
HeroTitle = response.HeroTitle,
HeroSubtitle = response.HeroSubtitle,
HeroImagePath = response.HeroImagePath,
IsActive = response.IsActive,
SettingsJson = response.SettingsJson,
Images = response.Images.Select(i => new PageSettingsImageDto
{
Id = i.Id,
ImageGroup = i.ImageGroup,
Title = i.Title,
Subtitle = i.Subtitle,
Description = i.Description,
ImagePath = i.ImagePath,
ThumbnailPath = i.ThumbnailPath,
LinkUrl = i.LinkUrl,
IconName = i.IconName,
SortOrder = i.SortOrder,
IsActive = i.IsActive
}).OrderBy(i => i.SortOrder).ToList()
};
}
// ── DTOs ──
public class PageSettingsDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public string? HeroImagePath { get; set; }
public bool IsActive { get; set; }
public string? SettingsJson { get; set; }
public List<PageSettingsImageDto> Images { get; set; } = new();
/// <summary>
/// دریافت تصاویر یک گروه خاص
/// </summary>
public List<PageSettingsImageDto> GetImages(string group) =>
Images.Where(i => i.ImageGroup == group && i.IsActive)
.OrderBy(i => i.SortOrder).ToList();
/// <summary>
/// دریافت تنظیمات JSON به عنوان یک نوع مشخص
/// </summary>
public T? GetSettings<T>() where T : class
{
if (string.IsNullOrWhiteSpace(SettingsJson)) return null;
try
{
return JsonSerializer.Deserialize<T>(SettingsJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch
{
return null;
}
}
}
public class PageSettingsImageDto
{
public long Id { get; set; }
public string ImageGroup { get; set; } = string.Empty;
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string ImagePath { get; set; } = string.Empty;
public string? ThumbnailPath { get; set; }
public string? LinkUrl { get; set; }
public string? IconName { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
@@ -1,18 +0,0 @@
{
"GwUrl": "https://cms.kbs2.ir",
"DownloadUrl": "",
"EncryptionSettings": {
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
},
"SignalR": {
"HubPath": "/hubs/token-notification"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
-1
View File
@@ -1,5 +1,4 @@
{
// "GwUrl": "https://localhost:34781",
"GwUrl": "https://localhost:32846",
"DownloadUrl": "",
"EncryptionSettings": {