feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
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:
masoodafar-web
2026-02-16 00:51:39 +03:30
parent f0d1156457
commit 6db83ccd90
104 changed files with 7668 additions and 1370 deletions
@@ -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();
}