feat: Add Licenses page + SitePageSettingsService
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m18s

- SitePageSettingsService: new gRPC client for simplified page settings
- Licenses.razor: new page showing license certificates from CMS
- RouteConstants: added Licenses route
- Footer + MainLayout: added Licenses navigation link
- Proto NuGet updated to 0.0.180
This commit is contained in:
masoodafar-web
2026-02-17 22:35:20 +03:30
parent 183ce0a2e5
commit bba9c1d1d8
8 changed files with 270 additions and 1 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" />
+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;
}
}
}
+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>
@@ -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)
@@ -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; }
}