feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s
- Discount store pages (products, cart, orders, order detail) - Blog pages and services - Payment gateway callback page - AppImage component, EmptyState, LoadingState, PageHeader - DiscountCartService, DiscountOrderService, DiscountProductService - BlogCategoryService, BlogPostService, SitePageService, ImageCacheService - PhoneVerifyForm component - Profile Hub page - UI/UX improvements across all pages - landing.js for homepage - Config and routing updates
This commit is contained in:
@@ -6,7 +6,12 @@ public class AuthDialogService
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDeviceDetector _deviceDetector;
|
||||
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
|
||||
private readonly DialogOptions _dialogOptions = new()
|
||||
{
|
||||
MaxWidth = MaxWidth.ExtraSmall,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
public AuthDialogService(IDialogService dialogService, IDeviceDetector deviceDetector)
|
||||
{
|
||||
@@ -18,12 +23,7 @@ public class AuthDialogService
|
||||
|
||||
public async Task ShowAuthDialogAsync()
|
||||
{
|
||||
// Pick dialog size based on device type
|
||||
var options = _deviceDetector.IsMobile()
|
||||
? new DialogOptions() { FullScreen = true}
|
||||
: _normalWidth;
|
||||
|
||||
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", options);
|
||||
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", _dialogOptions);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled)
|
||||
|
||||
@@ -54,7 +54,7 @@ public class AuthService
|
||||
}
|
||||
public bool IsCompleteRegister()
|
||||
{
|
||||
InitUserAuthInfo().GetAwaiter();
|
||||
InitUserAuthInfo().GetAwaiter().GetResult();
|
||||
if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract)
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Service for fetching active blog categories (customer-facing).
|
||||
/// </summary>
|
||||
public class BlogCategoryService
|
||||
{
|
||||
private readonly CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient _client;
|
||||
|
||||
public BlogCategoryService(CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all active blog categories (for blog sidebar / filter).
|
||||
/// </summary>
|
||||
public async Task<List<BlogCategoryDto>> GetActiveCategoriesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetActiveBlogCategoriesAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.BlogCategory.GetActiveBlogCategoriesRequest());
|
||||
|
||||
if (response?.Categories == null || response.Categories.Count == 0)
|
||||
return new();
|
||||
|
||||
return response.Categories
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new BlogCategoryDto
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
Slug = c.Slug,
|
||||
Description = c.Description,
|
||||
IconName = c.IconName,
|
||||
PostCount = c.PostCount
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"BlogCategoryService.GetActiveCategoriesAsync error: {ex.Message}");
|
||||
#endif
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── DTO ──
|
||||
public class BlogCategoryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int PostCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Service for fetching published blog posts (customer-facing).
|
||||
/// Used by Landing Page (latest posts) and future Blog pages.
|
||||
/// </summary>
|
||||
public class BlogPostService
|
||||
{
|
||||
private readonly CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient _client;
|
||||
|
||||
public BlogPostService(CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get featured blog posts (for landing page hero / highlights).
|
||||
/// </summary>
|
||||
public async Task<List<BlogPostCardDto>> GetFeaturedPostsAsync(int count = 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetFeaturedBlogPostsAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.BlogPost.GetFeaturedBlogPostsRequest { Count = count });
|
||||
|
||||
return MapToCardDtos(response?.Models);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"BlogPostService.GetFeaturedPostsAsync error: {ex.Message}");
|
||||
#endif
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get latest published posts with optional category filter.
|
||||
/// </summary>
|
||||
public async Task<BlogPostListResult> GetPublishedPostsAsync(int page = 1, int pageSize = 9, string? search = null, long? categoryId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new CMSMicroservice.Protobuf.Protos.BlogPost.GetPublishedBlogPostsRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||
{
|
||||
PageNumber = page,
|
||||
PageSize = pageSize
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
request.SearchTerm = search;
|
||||
if (categoryId.HasValue)
|
||||
request.CategoryId = categoryId.Value;
|
||||
|
||||
var response = await _client.GetPublishedBlogPostsAsync(request);
|
||||
|
||||
return new BlogPostListResult
|
||||
{
|
||||
Posts = MapToCardDtos(response?.Models),
|
||||
TotalCount = (int)(response?.MetaData?.TotalCount ?? 0),
|
||||
TotalPages = (int)(response?.MetaData?.TotalPage ?? 0),
|
||||
CurrentPage = page
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"BlogPostService.GetPublishedPostsAsync error: {ex.Message}");
|
||||
#endif
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a single blog post by slug (for blog detail page).
|
||||
/// </summary>
|
||||
public async Task<BlogPostDetailDto?> GetBySlugAsync(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetBlogPostBySlugAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.BlogPost.GetBlogPostBySlugRequest { Slug = slug });
|
||||
|
||||
if (response == null || response.Id <= 0) return null;
|
||||
|
||||
return new BlogPostDetailDto
|
||||
{
|
||||
Id = response.Id,
|
||||
Title = response.Title,
|
||||
Slug = response.Slug,
|
||||
Summary = response.Summary,
|
||||
HtmlContent = response.HtmlContent,
|
||||
FeaturedImagePath = response.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = response.FeaturedImageThumbnailPath,
|
||||
PublishedAt = response.PublishedAt?.ToDateTime(),
|
||||
ViewCount = response.ViewCount,
|
||||
IsFeatured = response.IsFeatured,
|
||||
Categories = response.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new(),
|
||||
Tags = response.Tags?.Select(t => new BlogTagInfo { Id = t.Id, Title = t.Title, Name = t.Name }).ToList() ?? new()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"BlogPostService.GetBySlugAsync error: {ex.Message}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increment view count for a blog post.
|
||||
/// </summary>
|
||||
public async Task IncrementViewCountAsync(long postId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.IncrementViewCountAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.BlogPost.IncrementViewCountRequest { Id = postId });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fire and forget — don't fail the page
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private static List<BlogPostCardDto> MapToCardDtos(
|
||||
Google.Protobuf.Collections.RepeatedField<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostListItem>? models)
|
||||
{
|
||||
if (models == null || models.Count == 0) return new();
|
||||
|
||||
return models.Select(m => new BlogPostCardDto
|
||||
{
|
||||
Id = m.Id,
|
||||
Title = m.Title,
|
||||
Slug = m.Slug,
|
||||
Summary = m.Summary,
|
||||
ThumbnailUrl = m.FeaturedImageThumbnailPath,
|
||||
PublishedAt = m.PublishedAt?.ToDateTime(),
|
||||
ViewCount = m.ViewCount,
|
||||
IsFeatured = m.IsFeatured,
|
||||
Categories = m.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── DTOs ──
|
||||
|
||||
public class BlogPostCardDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string? ThumbnailUrl { get; set; }
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public List<BlogCategoryInfo> Categories { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BlogPostDetailDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string HtmlContent { get; set; } = string.Empty;
|
||||
public string? FeaturedImagePath { get; set; }
|
||||
public string? FeaturedImageThumbnailPath { get; set; }
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public List<BlogCategoryInfo> Categories { get; set; } = new();
|
||||
public List<BlogTagInfo> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BlogPostListResult
|
||||
{
|
||||
public List<BlogPostCardDto> Posts { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int CurrentPage { get; set; }
|
||||
}
|
||||
|
||||
public class BlogCategoryInfo
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BlogTagInfo
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ public class CartService
|
||||
cartId:model.Id,
|
||||
ProductId: model.ProductId,
|
||||
Title: model.ProductTitle ?? string.Empty,
|
||||
ImageUrl: string.IsNullOrWhiteSpace(model.ProductThumbnailPath) ? string.Empty : UrlUtility.DownloadUrl + model.ProductThumbnailPath,
|
||||
ImageUrl: model.ProductThumbnailPath,
|
||||
UnitPrice: model.ProductPrice,
|
||||
Quantity: model.Count > 0 ? model.Count : 1)
|
||||
{
|
||||
|
||||
@@ -4,87 +4,162 @@ namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public static class CustomMudTheme
|
||||
{
|
||||
// ── Shared font stack ──
|
||||
private static readonly string[] VazirFontFamily = ["Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif"];
|
||||
|
||||
public static MudTheme CustomMudBlazorTheme { get; set; } = new()
|
||||
{
|
||||
// ═══════════════════════════════════════
|
||||
// Light Palette
|
||||
// ═══════════════════════════════════════
|
||||
PaletteLight = new PaletteLight()
|
||||
{
|
||||
Primary = "#0380C0",
|
||||
Background = "#F5F5F5",
|
||||
AppbarBackground = "#F5F5F5",
|
||||
TextPrimary = Colors.Gray.Darken3,
|
||||
Primary = "#6366f1", // Indigo — هویت اصلی برند
|
||||
PrimaryDarken = "#4f46e5",
|
||||
PrimaryLighten = "#818cf8",
|
||||
PrimaryContrastText = "#FFFFFF",
|
||||
Secondary = "#8b5cf6", // Purple
|
||||
SecondaryDarken = "#7c3aed",
|
||||
SecondaryLighten = "#a78bfa",
|
||||
SecondaryContrastText = "#FFFFFF",
|
||||
Tertiary = "#06b6d4", // Cyan — accent
|
||||
Info = "#3b82f6",
|
||||
InfoContrastText = "#FFFFFF",
|
||||
Success = "#10b981",
|
||||
SuccessContrastText = "#FFFFFF",
|
||||
Warning = "#f59e0b",
|
||||
WarningContrastText = "#FFFFFF",
|
||||
Error = "#ef4444",
|
||||
ErrorContrastText = "#FFFFFF",
|
||||
Background = "#f8f9fc", // Slightly cooler than plain gray
|
||||
Surface = "#FFFFFF",
|
||||
Divider = "#B2BFCB",
|
||||
AppbarBackground = "#f8f9fc",
|
||||
AppbarText = "#424242",
|
||||
TextPrimary = "#1e293b", // Slate 800
|
||||
TextSecondary = "#64748b", // Slate 500
|
||||
TextDisabled = "#94a3b8", // Slate 400
|
||||
Divider = "#e2e8f0", // Slate 200 — softer than old #B2BFCB
|
||||
DrawerBackground = "#FFFFFF",
|
||||
DrawerText = "#1e293b",
|
||||
DrawerIcon = "#64748b",
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// Dark Palette
|
||||
// ═══════════════════════════════════════
|
||||
PaletteDark = new PaletteDark()
|
||||
{
|
||||
Primary = "#818cf8", // Lighter indigo for dark BG
|
||||
PrimaryDarken = "#6366f1",
|
||||
PrimaryLighten = "#a5b4fc",
|
||||
PrimaryContrastText = "#0f0f23",
|
||||
Secondary = "#a78bfa",
|
||||
SecondaryDarken = "#8b5cf6",
|
||||
SecondaryLighten = "#c4b5fd",
|
||||
SecondaryContrastText = "#0f0f23",
|
||||
Tertiary = "#22d3ee",
|
||||
Info = "#60a5fa",
|
||||
InfoContrastText = "#0f172a",
|
||||
Success = "#34d399",
|
||||
SuccessContrastText = "#0f172a",
|
||||
Warning = "#fbbf24",
|
||||
WarningContrastText = "#0f172a",
|
||||
Error = "#f87171",
|
||||
ErrorContrastText = "#0f172a",
|
||||
Background = "#0f172a", // Slate 900
|
||||
Surface = "#1e293b", // Slate 800
|
||||
AppbarBackground = "#0f172a",
|
||||
AppbarText = "#e2e8f0",
|
||||
TextPrimary = "#f1f5f9", // Slate 100
|
||||
TextSecondary = "#94a3b8", // Slate 400
|
||||
TextDisabled = "#475569", // Slate 600
|
||||
Divider = "#334155", // Slate 700
|
||||
DrawerBackground = "#1e293b",
|
||||
DrawerText = "#f1f5f9",
|
||||
DrawerIcon = "#94a3b8",
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// Layout Properties
|
||||
// ═══════════════════════════════════════
|
||||
LayoutProperties = new LayoutProperties
|
||||
{
|
||||
DefaultBorderRadius = "12px",
|
||||
AppbarHeight = "56px",
|
||||
DrawerWidthLeft = "280px",
|
||||
DrawerWidthRight = "280px",
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// Typography
|
||||
// ═══════════════════════════════════════
|
||||
Typography = new Typography
|
||||
{
|
||||
Default = new DefaultTypography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" }
|
||||
},
|
||||
Default = new DefaultTypography() { FontFamily = VazirFontFamily },
|
||||
H1 = new H1Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = "1.70", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "2rem", LineHeight = "1.70",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
H2 = new H2Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = "1.65", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1.875rem", LineHeight = "1.65",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
H3 = new H3Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = "1.60", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1.5rem", LineHeight = "1.60",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
H4 = new H4Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = "1.55", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1.25rem", LineHeight = "1.55",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
H5 = new H5Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = "1.50", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1.125rem", LineHeight = "1.50",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
H6 = new H6Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.45", FontWeight = "800",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.45",
|
||||
FontWeight = "800", LetterSpacing = "normal"
|
||||
},
|
||||
Subtitle1 = new Subtitle1Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.62", FontWeight = "500",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.62",
|
||||
FontWeight = "500", LetterSpacing = "normal"
|
||||
},
|
||||
Subtitle2 = new Subtitle2Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "500",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.60",
|
||||
FontWeight = "500", LetterSpacing = "normal"
|
||||
},
|
||||
Body1 = new Body1Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.85", FontWeight = "400",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "1rem", LineHeight = "1.85",
|
||||
FontWeight = "400", LetterSpacing = "normal"
|
||||
},
|
||||
Body2 = new Body2Typography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.80", FontWeight = "400",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.80",
|
||||
FontWeight = "400", LetterSpacing = "normal"
|
||||
},
|
||||
Caption = new CaptionTypography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "400",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "0.75rem", LineHeight = "1.60",
|
||||
FontWeight = "400", LetterSpacing = "normal"
|
||||
},
|
||||
Overline = new OverlineTypography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "500",
|
||||
LetterSpacing = "normal"
|
||||
FontFamily = VazirFontFamily, FontSize = "0.75rem", LineHeight = "1.60",
|
||||
FontWeight = "500", LetterSpacing = "normal"
|
||||
},
|
||||
Button = new ButtonTypography()
|
||||
{
|
||||
FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "600",
|
||||
LetterSpacing = "normal", TextTransform = "none"
|
||||
FontFamily = VazirFontFamily, FontSize = "0.875rem", LineHeight = "1.60",
|
||||
FontWeight = "600", LetterSpacing = "normal", TextTransform = "none"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
|
||||
using Blazored.LocalStorage;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public record DiscountCartItem(
|
||||
long ProductId,
|
||||
string Title,
|
||||
string ImageUrl,
|
||||
long UnitPrice,
|
||||
int MaxDiscountPercent,
|
||||
int Quantity,
|
||||
long TotalPrice,
|
||||
long DiscountAmount,
|
||||
long FinalPrice,
|
||||
int RemainingCount);
|
||||
|
||||
public class DiscountCartService
|
||||
{
|
||||
private readonly DiscountShoppingCartContract.DiscountShoppingCartContractClient _client;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly List<DiscountCartItem> _items = new();
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
private bool _isInitialized;
|
||||
|
||||
public event Action? OnChange;
|
||||
|
||||
public DiscountCartService(
|
||||
DiscountShoppingCartContract.DiscountShoppingCartContractClient client,
|
||||
ILocalStorageService localStorage)
|
||||
{
|
||||
_client = client;
|
||||
_localStorage = localStorage;
|
||||
}
|
||||
|
||||
public IReadOnlyList<DiscountCartItem> Items => _items.AsReadOnly();
|
||||
public long TotalPrice => _items.Sum(i => i.TotalPrice);
|
||||
public long TotalDiscount => _items.Sum(i => i.DiscountAmount);
|
||||
public long FinalPrice => _items.Sum(i => i.FinalPrice);
|
||||
public int Count => _items.Sum(i => i.Quantity);
|
||||
|
||||
public async Task AddAsync(long productId, int count = 1)
|
||||
{
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
try
|
||||
{
|
||||
await _client.AddToCartAsync(new AddToCartRequest
|
||||
{
|
||||
ProductId = productId,
|
||||
Count = count
|
||||
});
|
||||
await LoadFromServerAsync();
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
public async Task UpdateQuantityAsync(long productId, int newCount)
|
||||
{
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
if (newCount <= 0)
|
||||
{
|
||||
await RemoveAsync(productId);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _client.UpdateCartItemCountAsync(new UpdateCartItemCountRequest
|
||||
{
|
||||
ProductId = productId,
|
||||
NewCount = newCount
|
||||
});
|
||||
await LoadFromServerAsync();
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(long productId)
|
||||
{
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
try
|
||||
{
|
||||
await _client.RemoveFromCartAsync(new RemoveFromCartRequest
|
||||
{
|
||||
ProductId = productId
|
||||
});
|
||||
_items.RemoveAll(i => i.ProductId == productId);
|
||||
Notify();
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
try
|
||||
{
|
||||
await _client.ClearCartAsync(new ClearCartRequest());
|
||||
_items.Clear();
|
||||
Notify();
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
public async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
if (await IsAuthenticatedAsync())
|
||||
{
|
||||
await LoadFromServerAsync();
|
||||
}
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private async Task LoadFromServerAsync()
|
||||
{
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
try
|
||||
{
|
||||
var response = await _client.GetUserCartAsync(new GetUserCartRequest());
|
||||
_items.Clear();
|
||||
foreach (var item in response.Items)
|
||||
{
|
||||
_items.Add(new DiscountCartItem(
|
||||
ProductId: item.ProductId,
|
||||
Title: item.ProductTitle ?? string.Empty,
|
||||
ImageUrl: item.ProductImagePath,
|
||||
UnitPrice: item.UnitPrice,
|
||||
MaxDiscountPercent: item.MaxDiscountPercent,
|
||||
Quantity: item.Count,
|
||||
TotalPrice: item.TotalPrice,
|
||||
DiscountAmount: item.DiscountAmount,
|
||||
FinalPrice: item.FinalPrice,
|
||||
RemainingCount: item.ProductRemainingCount));
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
catch { /* fallback to local state */ }
|
||||
}
|
||||
|
||||
private void Notify() => OnChange?.Invoke();
|
||||
|
||||
private async Task<bool> IsAuthenticatedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await _localStorage.GetItemAsync<string>(TokenStorageKey);
|
||||
return !string.IsNullOrWhiteSpace(token);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
// ── DTOs ──
|
||||
public record DiscountOrderSummary(
|
||||
long Id,
|
||||
string OrderNumber,
|
||||
long TotalPrice,
|
||||
long DiscountBalanceUsed,
|
||||
long GatewayAmount,
|
||||
bool PaymentCompleted,
|
||||
int DeliveryStatus,
|
||||
string? TrackingCode,
|
||||
int ItemsCount,
|
||||
DateTime Created);
|
||||
|
||||
public record DiscountOrderDetail(
|
||||
long Id,
|
||||
string OrderNumber,
|
||||
long TotalPrice,
|
||||
long DiscountBalanceUsed,
|
||||
long GatewayAmount,
|
||||
bool PaymentCompleted,
|
||||
string? TransactionId,
|
||||
int DeliveryStatus,
|
||||
string? TrackingCode,
|
||||
string? Notes,
|
||||
string? AdminNotes,
|
||||
DiscountOrderAddress? Address,
|
||||
List<DiscountOrderItem> Items,
|
||||
DateTime Created);
|
||||
|
||||
public record DiscountOrderAddress(
|
||||
long Id, string Title, string Address, string PostalCode, string? Phone);
|
||||
|
||||
public record DiscountOrderItem(
|
||||
long ProductId,
|
||||
string Title,
|
||||
long UnitPrice,
|
||||
int MaxDiscountPercent,
|
||||
int Count,
|
||||
long TotalPrice,
|
||||
long DiscountAmount,
|
||||
long FinalPrice);
|
||||
|
||||
public record PlaceDiscountOrderResult(
|
||||
bool Success,
|
||||
string Message,
|
||||
long OrderId,
|
||||
long GatewayAmount,
|
||||
string? PaymentUrl);
|
||||
|
||||
public record DiscountOrderListResult(
|
||||
List<DiscountOrderSummary> Orders,
|
||||
int TotalCount,
|
||||
int TotalPages,
|
||||
int CurrentPage);
|
||||
|
||||
// ── Service ──
|
||||
public class DiscountOrderService
|
||||
{
|
||||
private readonly DiscountOrderContract.DiscountOrderContractClient _client;
|
||||
|
||||
public DiscountOrderService(DiscountOrderContract.DiscountOrderContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<PlaceDiscountOrderResult> PlaceOrderAsync(long addressId, long discountBalanceToUse, string? notes = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new PlaceOrderRequest
|
||||
{
|
||||
UserAddressId = addressId,
|
||||
DiscountBalanceToUse = discountBalanceToUse
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(notes))
|
||||
request.Notes = notes;
|
||||
|
||||
var response = await _client.PlaceOrderAsync(request);
|
||||
return new PlaceDiscountOrderResult(
|
||||
Success: response.Success,
|
||||
Message: response.Message ?? string.Empty,
|
||||
OrderId: response.OrderId,
|
||||
GatewayAmount: response.GatewayAmount,
|
||||
PaymentUrl: response.PaymentUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PlaceDiscountOrderResult(false, ex.Message, 0, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CompletePaymentAsync(long orderId, string? transactionId, bool success)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.CompleteOrderPaymentAsync(new CompleteOrderPaymentRequest
|
||||
{
|
||||
OrderId = orderId,
|
||||
TransactionId = transactionId,
|
||||
PaymentSuccess = success
|
||||
});
|
||||
return response.Success;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<DiscountOrderListResult> GetUserOrdersAsync(int page = 1, int pageSize = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetUserOrdersAsync(new GetUserOrdersRequest
|
||||
{
|
||||
PageNumber = page,
|
||||
PageSize = pageSize
|
||||
});
|
||||
|
||||
var orders = response.Models.Select(m => new DiscountOrderSummary(
|
||||
Id: m.Id,
|
||||
OrderNumber: m.OrderNumber ?? string.Empty,
|
||||
TotalPrice: m.TotalPrice,
|
||||
DiscountBalanceUsed: m.DiscountBalanceUsed,
|
||||
GatewayAmount: m.GatewayAmount,
|
||||
PaymentCompleted: m.PaymentCompleted,
|
||||
DeliveryStatus: (int)m.DeliveryStatus,
|
||||
TrackingCode: m.TrackingCode,
|
||||
ItemsCount: m.ItemsCount,
|
||||
Created: m.Created?.ToDateTime() ?? DateTime.MinValue
|
||||
)).ToList();
|
||||
|
||||
var totalCount = (int)(response.MetaData?.TotalCount ?? 0);
|
||||
var totalPages = (int)(response.MetaData?.TotalPage ?? 0);
|
||||
|
||||
return new DiscountOrderListResult(orders, totalCount, totalPages, page);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DiscountOrderListResult(new(), 0, 0, page);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DiscountOrderDetail?> GetOrderByIdAsync(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var r = await _client.GetOrderByIdAsync(new GetOrderByIdRequest { OrderId = orderId });
|
||||
|
||||
var address = r.Address != null
|
||||
? new DiscountOrderAddress(r.Address.Id, r.Address.Title ?? "", r.Address.Address ?? "", r.Address.PostalCode ?? "", r.Address.Phone)
|
||||
: null;
|
||||
|
||||
var items = r.Items.Select(i => new DiscountOrderItem(
|
||||
ProductId: i.ProductId,
|
||||
Title: i.ProductTitle ?? string.Empty,
|
||||
UnitPrice: i.UnitPrice,
|
||||
MaxDiscountPercent: i.MaxDiscountPercent,
|
||||
Count: i.Count,
|
||||
TotalPrice: i.TotalPrice,
|
||||
DiscountAmount: i.DiscountAmount,
|
||||
FinalPrice: i.FinalPrice
|
||||
)).ToList();
|
||||
|
||||
return new DiscountOrderDetail(
|
||||
Id: r.Id,
|
||||
OrderNumber: r.OrderNumber ?? string.Empty,
|
||||
TotalPrice: r.TotalPrice,
|
||||
DiscountBalanceUsed: r.DiscountBalanceUsed,
|
||||
GatewayAmount: r.GatewayAmount,
|
||||
PaymentCompleted: r.PaymentCompleted,
|
||||
TransactionId: r.TransactionId,
|
||||
DeliveryStatus: (int)r.DeliveryStatus,
|
||||
TrackingCode: r.TrackingCode,
|
||||
Notes: r.Notes,
|
||||
AdminNotes: r.AdminNotes,
|
||||
Address: address,
|
||||
Items: items,
|
||||
Created: r.Created?.ToDateTime() ?? DateTime.MinValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetDeliveryStatusText(int status) => status switch
|
||||
{
|
||||
0 => "در انتظار",
|
||||
1 => "در حال پردازش",
|
||||
2 => "ارسال شده",
|
||||
3 => "تحویل داده شده",
|
||||
4 => "لغو شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
public static Color GetDeliveryStatusColor(int status) => status switch
|
||||
{
|
||||
0 => Color.Warning,
|
||||
1 => Color.Info,
|
||||
2 => Color.Primary,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
// ── DTOs ──
|
||||
public record DiscountProductCard(
|
||||
long Id,
|
||||
string Title,
|
||||
string ShortInfo,
|
||||
long Price,
|
||||
int MaxDiscountPercent,
|
||||
string ImageUrl,
|
||||
string ThumbnailUrl,
|
||||
int RemainingCount,
|
||||
int ViewCount,
|
||||
bool IsActive,
|
||||
DateTime Created);
|
||||
|
||||
public record DiscountProductDetail(
|
||||
long Id,
|
||||
string Title,
|
||||
string ShortInfo,
|
||||
string FullInfo,
|
||||
long Price,
|
||||
int MaxDiscountPercent,
|
||||
string ImageUrl,
|
||||
string ThumbnailUrl,
|
||||
int RemainingCount,
|
||||
int ViewCount,
|
||||
int SortOrder,
|
||||
bool IsActive,
|
||||
List<DiscountCategoryInfo> Categories,
|
||||
List<DiscountProductImageDto> Images,
|
||||
DateTime Created);
|
||||
|
||||
public record DiscountCategoryInfo(long Id, string Name, string Title);
|
||||
|
||||
public record DiscountProductImageDto(
|
||||
long Id,
|
||||
string ImageUrl,
|
||||
string ThumbnailUrl,
|
||||
string? Title,
|
||||
string? AltText,
|
||||
int SortOrder);
|
||||
|
||||
public record DiscountCategoryNode(
|
||||
long Id,
|
||||
string Name,
|
||||
string Title,
|
||||
string? Description,
|
||||
string? ImagePath,
|
||||
long? ParentCategoryId,
|
||||
int SortOrder,
|
||||
bool IsActive,
|
||||
int ProductCount,
|
||||
List<DiscountCategoryNode> Children);
|
||||
|
||||
public record DiscountProductListResult(
|
||||
List<DiscountProductCard> Products,
|
||||
int TotalCount,
|
||||
int TotalPages,
|
||||
int CurrentPage);
|
||||
|
||||
// ── Service ──
|
||||
public class DiscountProductService
|
||||
{
|
||||
private readonly DiscountProductContract.DiscountProductContractClient _productClient;
|
||||
private readonly DiscountCategoryContract.DiscountCategoryContractClient _categoryClient;
|
||||
|
||||
public DiscountProductService(
|
||||
DiscountProductContract.DiscountProductContractClient productClient,
|
||||
DiscountCategoryContract.DiscountCategoryContractClient categoryClient)
|
||||
{
|
||||
_productClient = productClient;
|
||||
_categoryClient = categoryClient;
|
||||
}
|
||||
|
||||
public async Task<DiscountProductListResult> GetProductsAsync(
|
||||
int page = 1, int pageSize = 12,
|
||||
string? search = null, long? categoryId = null,
|
||||
bool? inStock = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetDiscountProductsRequest
|
||||
{
|
||||
PageNumber = page,
|
||||
PageSize = pageSize,
|
||||
IsActive = true // فقط محصولات فعال
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
request.SearchQuery = search;
|
||||
if (categoryId.HasValue)
|
||||
request.CategoryId = categoryId.Value;
|
||||
if (inStock.HasValue)
|
||||
request.InStock = inStock.Value;
|
||||
|
||||
var response = await _productClient.GetDiscountProductsAsync(request);
|
||||
|
||||
var products = response.Models.Select(m => new DiscountProductCard(
|
||||
Id: m.Id,
|
||||
Title: m.Title ?? string.Empty,
|
||||
ShortInfo: m.ShortInfomation ?? string.Empty,
|
||||
Price: m.Price,
|
||||
MaxDiscountPercent: m.MaxDiscountPercent,
|
||||
ImageUrl: BuildUrl(m.ImagePath),
|
||||
ThumbnailUrl: BuildUrl(m.ThumbnailPath),
|
||||
RemainingCount: m.RemainingCount,
|
||||
ViewCount: m.ViewCount,
|
||||
IsActive: m.IsActive,
|
||||
Created: m.Created?.ToDateTime() ?? DateTime.MinValue
|
||||
)).ToList();
|
||||
|
||||
var totalCount = (int)(response.MetaData?.TotalCount ?? 0);
|
||||
var totalPages = (int)(response.MetaData?.TotalPage ?? 0);
|
||||
|
||||
return new DiscountProductListResult(products, totalCount, totalPages, page);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DiscountProductListResult(new(), 0, 0, page);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _productClient.GetDiscountProductByIdAsync(
|
||||
new GetDiscountProductByIdRequest { ProductId = productId });
|
||||
|
||||
var categories = response.Categories
|
||||
.Select(c => new DiscountCategoryInfo(c.Id, c.Name ?? "", c.Title ?? ""))
|
||||
.ToList();
|
||||
|
||||
// Load images
|
||||
var imagesResponse = await _productClient.GetDiscountProductImagesAsync(
|
||||
new GetDiscountProductImagesRequest { DiscountProductId = productId, OnlyActive = true });
|
||||
|
||||
var images = imagesResponse.Images
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.Select(i => new DiscountProductImageDto(
|
||||
Id: i.Id,
|
||||
ImageUrl: BuildUrl(i.ImagePath),
|
||||
ThumbnailUrl: BuildUrl(i.ThumbnailPath),
|
||||
Title: i.Title,
|
||||
AltText: i.AltText,
|
||||
SortOrder: i.SortOrder))
|
||||
.ToList();
|
||||
|
||||
return new DiscountProductDetail(
|
||||
Id: response.Id,
|
||||
Title: response.Title ?? string.Empty,
|
||||
ShortInfo: response.ShortInfomation ?? string.Empty,
|
||||
FullInfo: response.FullInformation ?? string.Empty,
|
||||
Price: response.Price,
|
||||
MaxDiscountPercent: response.MaxDiscountPercent,
|
||||
ImageUrl: BuildUrl(response.ImagePath),
|
||||
ThumbnailUrl: BuildUrl(response.ThumbnailPath),
|
||||
RemainingCount: response.RemainingCount,
|
||||
ViewCount: response.ViewCount,
|
||||
SortOrder: response.SortOrder,
|
||||
IsActive: response.IsActive,
|
||||
Categories: categories,
|
||||
Images: images,
|
||||
Created: response.Created?.ToDateTime() ?? DateTime.MinValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<DiscountCategoryNode>> GetCategoriesAsync(bool onlyActive = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetDiscountCategoriesRequest();
|
||||
if (onlyActive) request.IsActive = true;
|
||||
|
||||
var response = await _categoryClient.GetDiscountCategoriesAsync(request);
|
||||
return response.Categories.Select(MapCategory).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
private static DiscountCategoryNode MapCategory(DiscountCategoryDto dto) => new(
|
||||
Id: dto.Id,
|
||||
Name: dto.Name ?? "",
|
||||
Title: dto.Title ?? "",
|
||||
Description: dto.Description,
|
||||
ImagePath: dto.ImagePath,
|
||||
ParentCategoryId: dto.ParentCategoryId,
|
||||
SortOrder: dto.SortOrder,
|
||||
IsActive: dto.IsActive,
|
||||
ProductCount: dto.ProductCount,
|
||||
Children: dto.Children.Select(MapCategory).ToList());
|
||||
|
||||
private static string BuildUrl(string? path)
|
||||
=> path ?? string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Concurrent;
|
||||
using CMSMicroservice.Protobuf.Protos.ImageResolver;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مرکزی resolve تصاویر — تمام تصاویر دینامیک FrontOffice از این سرویس عبور میکنند.
|
||||
/// مسیرهای نسبی از طریق gRPC به CMS ارسال شده و base64 data-URI دریافت میشود.
|
||||
/// نتایج در حافظه کش میشوند تا از فراخوانی مجدد جلوگیری شود.
|
||||
/// </summary>
|
||||
public class ImageCacheService
|
||||
{
|
||||
private readonly ImageResolverContract.ImageResolverContractClient _client;
|
||||
private readonly ConcurrentDictionary<string, string> _cache = new();
|
||||
|
||||
public ImageCacheService(ImageResolverContract.ImageResolverContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل یک مسیر تصویر به base64 data-URI.
|
||||
/// اگر مسیر خالی باشد → رشته خالی.
|
||||
/// اگر قبلاً data-URI باشد → بدون تغییر.
|
||||
/// اگر در کش باشد → از کش.
|
||||
/// در غیر این صورت → فراخوانی gRPC.
|
||||
/// </summary>
|
||||
public async Task<string> ResolveAsync(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return string.Empty;
|
||||
|
||||
// اگر قبلاً base64 هست (مثلاً از interceptor آمده)
|
||||
if (path.StartsWith("data:", StringComparison.Ordinal))
|
||||
return path;
|
||||
|
||||
// مسیرهای استاتیک (wwwroot) یا لینکهای خارجی نیاز به resolve ندارند
|
||||
if (path.StartsWith('/') || path.StartsWith("http://", StringComparison.Ordinal)
|
||||
|| path.StartsWith("https://", StringComparison.Ordinal))
|
||||
return path;
|
||||
|
||||
// جستجو در کش
|
||||
if (_cache.TryGetValue(path, out var cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _client.ResolveImagesAsync(
|
||||
new ResolveImagesRequest { Paths = { path } });
|
||||
|
||||
var result = response?.Images?.FirstOrDefault()?.DataUri ?? string.Empty;
|
||||
_cache.TryAdd(path, result);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// در صورت خطا، رشته خالی برگردان تا UI خراب نشود
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل دستهای مسیرهای تصویر به base64 data-URI.
|
||||
/// مسیرهای کششده و data-URIها بدون فراخوانی gRPC برگردانده میشوند.
|
||||
/// </summary>
|
||||
public async Task<Dictionary<string, string>> ResolveBatchAsync(IEnumerable<string?> paths)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
var toResolve = new List<string>();
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
continue;
|
||||
|
||||
if (path.StartsWith("data:", StringComparison.Ordinal)
|
||||
|| path.StartsWith('/')
|
||||
|| path.StartsWith("http://", StringComparison.Ordinal)
|
||||
|| path.StartsWith("https://", StringComparison.Ordinal))
|
||||
{
|
||||
result[path] = path;
|
||||
}
|
||||
else if (_cache.TryGetValue(path, out var cached))
|
||||
{
|
||||
result[path] = cached;
|
||||
}
|
||||
else
|
||||
{
|
||||
toResolve.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (toResolve.Count == 0)
|
||||
return result;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _client.ResolveImagesAsync(
|
||||
new ResolveImagesRequest { Paths = { toResolve } });
|
||||
|
||||
if (response?.Images != null)
|
||||
{
|
||||
foreach (var img in response.Images)
|
||||
{
|
||||
_cache.TryAdd(img.OriginalPath, img.DataUri);
|
||||
result[img.OriginalPath] = img.DataUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// در صورت خطا، مسیرهای resolve نشده خالی بمانند
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پاکسازی تمام کش (مثلاً هنگام logout)
|
||||
/// </summary>
|
||||
public void ClearCache() => _cache.Clear();
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class OrderService
|
||||
|
||||
public async Task<List<GetUserOrderResponse>> GetOrdersAsync()
|
||||
{
|
||||
var result = await _userOrderContractClient.GetAllUserOrderByFilterAsync(new());
|
||||
var result = await _userOrderContractClient.GetCustomerOrdersAsync(new());
|
||||
if (result != null && result.Models.Count > 0)
|
||||
{
|
||||
foreach (var item in result.Models)
|
||||
@@ -69,7 +69,7 @@ public class OrderService
|
||||
var order = _orders.FirstOrDefault(o => o.Id == id);
|
||||
if (order == null)
|
||||
{
|
||||
var result = await _userOrderContractClient.GetUserOrderAsync(new GetUserOrderRequest()
|
||||
var result = await _userOrderContractClient.GetCustomerOrderAsync(new GetUserOrderRequest()
|
||||
{
|
||||
Id = id
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ public class PackageService
|
||||
m.Id,
|
||||
m.Title,
|
||||
m.Description,
|
||||
UrlUtility.DownloadUrl + m.ImagePath,
|
||||
m.ImagePath,
|
||||
m.Price))
|
||||
.ToList();
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class PackageService
|
||||
response.Id,
|
||||
response.Title,
|
||||
response.Description,
|
||||
UrlUtility.DownloadUrl + response.ImagePath,
|
||||
response.ImagePath,
|
||||
response.Price);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -141,7 +141,7 @@ public class ProductService
|
||||
Title: m.Title ?? string.Empty,
|
||||
Description: m.Description ?? string.Empty,
|
||||
FullInformation: m.FullInformation ?? string.Empty,
|
||||
ImageUrl: string.IsNullOrWhiteSpace(m.ImagePath) ? string.Empty : UrlUtility.DownloadUrl + m.ImagePath,
|
||||
ImageUrl: m.ImagePath,
|
||||
Price: m.Price,
|
||||
Discount: m.Discount,
|
||||
Rate: m.Rate,
|
||||
@@ -244,7 +244,7 @@ public class ProductService
|
||||
|
||||
private sealed record CacheEntry(Product Product, DateTime Expiration);
|
||||
private static string BuildUrl(string? path) =>
|
||||
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;
|
||||
path ?? string.Empty;
|
||||
|
||||
private static IReadOnlyList<ProductCategoryPathInfo> MapCategoryPaths(IEnumerable<ProductCategoryPath>? categories)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ public static class RouteConstants
|
||||
public static class Profile
|
||||
{
|
||||
public const string Index = "/profile";
|
||||
public const string Hub = "/profile/hub";
|
||||
public const string Personal = "/profile/personal";
|
||||
public const string Addresses = "/profile/addresses";
|
||||
public const string Settings = "/profile/settings";
|
||||
@@ -66,6 +67,12 @@ public static class RouteConstants
|
||||
public const string Index = "/contact";
|
||||
}
|
||||
|
||||
public static class Blog
|
||||
{
|
||||
public const string Index = "/blog";
|
||||
public const string Post = "/blog/"; // usage: /blog/{slug}
|
||||
}
|
||||
|
||||
public static class Checkout
|
||||
{
|
||||
public const string Index = "/checkout";
|
||||
@@ -82,4 +89,24 @@ public static class RouteConstants
|
||||
public const string OrderTracking = "/order-tracking/"; // usage: /order-tracking/{id}
|
||||
public const string Categories = "/categories";
|
||||
}
|
||||
|
||||
public static class DiscountStore
|
||||
{
|
||||
public const string Products = "/discount-store";
|
||||
public const string ProductDetail = "/discount-store/product/"; // usage: /discount-store/product/{id}
|
||||
public const string Cart = "/discount-store/cart";
|
||||
public const string Checkout = "/discount-store/checkout";
|
||||
public const string Orders = "/discount-store/orders";
|
||||
public const string OrderDetail = "/discount-store/order/"; // usage: /discount-store/order/{id}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gateway/chooser pages — when a concept exists in both stores
|
||||
/// </summary>
|
||||
public static class Gateway
|
||||
{
|
||||
public const string StoreChooser = "/stores";
|
||||
public const string OrdersChooser = "/my-orders";
|
||||
public const string CartChooser = "/my-cart";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public class SitePageService
|
||||
{
|
||||
private readonly CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient _client;
|
||||
|
||||
public SitePageService(CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<SitePageDto?> GetByKeyAsync(string pageKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetSitePageByKeyAsync(
|
||||
new CMSMicroservice.Protobuf.Protos.SitePage.GetSitePageByKeyRequest { PageKey = pageKey });
|
||||
|
||||
if (response == null || response.Id <= 0) return null;
|
||||
|
||||
var dto = new SitePageDto
|
||||
{
|
||||
Id = response.Id,
|
||||
PageKey = response.PageKey,
|
||||
Title = response.Title,
|
||||
MetaDescription = response.MetaDescription,
|
||||
HeroTitle = response.HeroTitle,
|
||||
HeroSubtitle = response.HeroSubtitle,
|
||||
HeroImagePath = response.HeroImagePath,
|
||||
IsActive = response.IsActive
|
||||
};
|
||||
|
||||
foreach (var s in response.Sections.OrderBy(s => s.SortOrder))
|
||||
{
|
||||
dto.Sections.Add(new SitePageSectionDto
|
||||
{
|
||||
Id = s.Id,
|
||||
SectionKey = s.SectionKey,
|
||||
Title = s.Title,
|
||||
Subtitle = s.Subtitle,
|
||||
HtmlContent = s.HtmlContent,
|
||||
IconName = s.IconName,
|
||||
ImagePath = s.ImagePath,
|
||||
ImageThumbnailPath = s.ImageThumbnailPath,
|
||||
SortOrder = s.SortOrder,
|
||||
IsActive = s.IsActive,
|
||||
ExtraData = s.ExtraData
|
||||
});
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"SitePageService.GetByKeyAsync error: {ex.Message}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SitePageDto
|
||||
{
|
||||
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 List<SitePageSectionDto> Sections { get; set; } = new();
|
||||
|
||||
public SitePageSectionDto? GetSection(string sectionKey) =>
|
||||
Sections.FirstOrDefault(s => s.SectionKey == sectionKey && s.IsActive);
|
||||
|
||||
public List<SitePageSectionDto> GetSections(string prefix) =>
|
||||
Sections.Where(s => s.SectionKey.StartsWith(prefix) && s.IsActive)
|
||||
.OrderBy(s => s.SortOrder).ToList();
|
||||
}
|
||||
|
||||
public class SitePageSectionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string SectionKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Subtitle { get; set; }
|
||||
public string? HtmlContent { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
public string? ImageThumbnailPath { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? ExtraData { get; set; }
|
||||
|
||||
public T? GetExtraData<T>() where T : class
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ExtraData)) return null;
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(ExtraData, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,16 @@
|
||||
public static class UrlUtility
|
||||
{
|
||||
public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null
|
||||
|
||||
/// <summary>
|
||||
/// ساخت URL کامل تصویر — اگر مسیر data-URI یا http باشد همان را برمیگرداند
|
||||
/// </summary>
|
||||
public static string GetImageUrl(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return string.Empty;
|
||||
if (path.StartsWith("data:") || path.StartsWith("http"))
|
||||
return path;
|
||||
return DownloadUrl + path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user