diff --git a/apps/web-client-eggymon-landipage-net/Dockerfile b/apps/web-client-eggymon-landipage-net/Dockerfile new file mode 100644 index 00000000..fed98436 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/Dockerfile @@ -0,0 +1,59 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# EN: Eggymon Kitchen Landing Page - Multi-stage Docker Build +# VI: Eggymon Kitchen Landing Page - Build Docker đa giai đoạn +# ═══════════════════════════════════════════════════════════════════════════════ + +# ─── Stage 1: Build ────────────────────────────────────────────────────────── +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# EN: Copy project files first for better layer caching +# VI: Copy file dự án trước để cache layer tốt hơn +COPY src/EggymonLandingPage.Shared/EggymonLandingPage.Shared.csproj src/EggymonLandingPage.Shared/ +COPY src/EggymonLandingPage.Client/EggymonLandingPage.Client.csproj src/EggymonLandingPage.Client/ +COPY src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj src/EggymonLandingPage.Server/ + +# EN: Restore dependencies +# VI: Restore các gói phụ thuộc +RUN dotnet restore src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj + +# EN: Copy all source code +# VI: Copy toàn bộ mã nguồn +COPY . . + +# EN: Build and Publish +# VI: Build và Publish +RUN dotnet publish src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj \ + -c Release \ + -o /app/publish \ + --no-restore + +# ─── Stage 2: Runtime ──────────────────────────────────────────────────────── +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime +WORKDIR /app + +# EN: Create non-root user for security +# VI: Tạo user non-root cho bảo mật +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# EN: Copy published output +# VI: Copy kết quả publish +COPY --from=build /app/publish . + +# EN: Set ownership to non-root user +# VI: Đặt quyền sở hữu cho user non-root +RUN chown -R appuser:appgroup /app + +USER appuser + +# EN: Expose port 8080 (default non-root port) +# VI: Expose cổng 8080 (cổng mặc định non-root) +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 + +# EN: Health check +# VI: Kiểm tra sức khỏe +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +ENTRYPOINT ["dotnet", "EggymonLandingPage.Server.dll"] diff --git a/apps/web-client-eggymon-landipage-net/EggymonLandingPage.slnx b/apps/web-client-eggymon-landipage-net/EggymonLandingPage.slnx new file mode 100644 index 00000000..1998498d --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/EggymonLandingPage.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web-client-eggymon-landipage-net/README.md b/apps/web-client-eggymon-landipage-net/README.md new file mode 100644 index 00000000..9b5500ba --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/README.md @@ -0,0 +1,63 @@ +# 🥚 Eggymon Kitchen Landing Page + +> **EN:** Multilingual landing page for Eggymon Kitchen — Built with Blazor WebAssembly + ASP.NET Core BFF +> **VI:** Trang landing đa ngôn ngữ cho Eggymon Kitchen — Xây dựng bằng Blazor WebAssembly + ASP.NET Core BFF + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ EggymonLandingPage.Server │ +│ (ASP.NET Core BFF + YARP Reverse Proxy) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ EggymonLandingPage.Client │ │ +│ │ (Blazor WebAssembly + MudBlazor) │ │ +│ │ ┌──────────────────────────────────────────┐ │ │ +│ │ │ EggymonLandingPage.Shared │ │ │ +│ │ │ (Shared DTOs) │ │ │ +│ │ └──────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Tech Stack + +| Layer | Technology | +|-----------|-------------------------------------| +| UI | Blazor WebAssembly (.NET 10) | +| Components| MudBlazor 8.15 | +| Server | ASP.NET Core BFF + YARP | +| Styling | CSS Variables (Primitives → Semantics → Components) | +| i18n | JSON-based localization (EN/VI) | +| Fonts | Fredoka (headings) + Inter (body) | +| Container | Docker multi-stage (Alpine) | + +## Getting Started + +```bash +# Run in development mode +cd src/EggymonLandingPage.Server +dotnet run + +# Build production +dotnet publish -c Release + +# Docker +docker build -t eggymon-landing . +docker run -p 8080:8080 eggymon-landing +``` + +## Routes + +| Route | Description | +|----------|---------------------| +| `/` | Landing page (EN) | +| `/en-US/`| English version | +| `/vi-VN/`| Vietnamese version | +| `/health`| Health check | + +## Design System + +🎨 **Brand Colors:** Warm Brown + Cream + Egg Yellow +🌙 **Dark Mode:** Full light/dark theme support +📱 **Responsive:** Mobile-first design with CSS Grid diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/App.razor b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/App.razor new file mode 100644 index 00000000..1d6d8563 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not Found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Components/LanguageSwitcher.razor b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Components/LanguageSwitcher.razor new file mode 100644 index 00000000..7c580c80 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Components/LanguageSwitcher.razor @@ -0,0 +1,66 @@ +@using System.Globalization +@inject NavigationManager Navigation + + + + + + @GetCurrentLabel() + + + + + + + + 🇻🇳 + Tiếng Việt + + + + + 🇺🇸 + English + + + + + +@code { + private string GetCurrentLabel() + { + var uri = new Uri(Navigation.Uri); + var path = uri.PathAndQuery; + + if (path.StartsWith("/vi", StringComparison.OrdinalIgnoreCase)) + { + return "VI"; + } + return "EN"; + } + + private void SwitchLanguage(string targetCulture) + { + var uri = new Uri(Navigation.Uri); + var path = uri.PathAndQuery; + + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + + string newPath; + if (segments.Length > 0 && (segments[0].Equals("vi-VN", StringComparison.OrdinalIgnoreCase) || + segments[0].Equals("en-US", StringComparison.OrdinalIgnoreCase) || + segments[0].Equals("vi", StringComparison.OrdinalIgnoreCase) || + segments[0].Equals("en", StringComparison.OrdinalIgnoreCase))) + { + segments[0] = targetCulture; + newPath = "/" + string.Join('/', segments); + } + else + { + if (path == "/") path = ""; + newPath = $"/{targetCulture}{path}"; + } + + Navigation.NavigateTo(newPath, forceLoad: true); + } +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/EggymonLandingPage.Client.csproj b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/EggymonLandingPage.Client.csproj new file mode 100644 index 00000000..5d6cf6b0 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/EggymonLandingPage.Client.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + true + true + + + + + + + + + + + + + + diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Layout/MainLayout.razor b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Layout/MainLayout.razor new file mode 100644 index 00000000..f92cfe94 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Layout/MainLayout.razor @@ -0,0 +1,62 @@ +@inherits LayoutComponentBase +@inject IStringLocalizer L + + + + + + + + + + + + + + + + + + + + + + + + + @Body + + + +@code { + [Inject] private IJSRuntime JSRuntime { get; set; } = default!; + + private bool _isDarkMode = false; + + private MudTheme _theme = new() + { + PaletteLight = new PaletteLight() + { + Primary = "#6B4423", + PrimaryContrastText = "#ffffff", + AppbarBackground = "#FFFFFF", + AppbarText = "#2C2C2C", + Background = "#FAF8F4", + Surface = "#ffffff", + TextPrimary = "#2C2C2C", + ActionDefault = "#2C2C2C", + LinesDefault = "#eeeeee" + }, + }; +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizer.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizer.cs new file mode 100644 index 00000000..b3249286 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizer.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using System.Net.Http.Json; +using Microsoft.Extensions.Localization; + +namespace EggymonLandingPage.Client.Localization; + +/// +/// EN: JSON-based string localizer for Blazor WASM. +/// VI: String localizer dựa trên JSON cho Blazor WASM. +/// +public class JsonStringLocalizer : IStringLocalizer +{ + private readonly LocalizationCache _cache; + private readonly string _resourceName; + + public JsonStringLocalizer(LocalizationCache cache, string resourceName) + { + _cache = cache; + _resourceName = resourceName; + } + + public JsonStringLocalizer(LocalizationCache cache) + { + _cache = cache; + _resourceName = "Shared"; + } + + public LocalizedString this[string name] + { + get + { + var value = GetString(name); + return new LocalizedString(name, value ?? name, resourceNotFound: value == null); + } + } + + public LocalizedString this[string name, params object[] arguments] + { + get + { + var format = GetString(name); + var value = string.Format(format ?? name, arguments); + return new LocalizedString(name, value, resourceNotFound: format == null); + } + } + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + return Enumerable.Empty(); + } + + private string? GetString(string name) + { + return _cache.GetString(name); + } +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizerFactory.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizerFactory.cs new file mode 100644 index 00000000..ae4c5050 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/JsonStringLocalizerFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Localization; + +namespace EggymonLandingPage.Client.Localization; + +/// +/// EN: Factory for creating JSON string localizers. +/// VI: Factory tạo JSON string localizer. +/// +public class JsonStringLocalizerFactory : IStringLocalizerFactory +{ + private readonly LocalizationCache _cache; + private readonly IServiceProvider _serviceProvider; + + public JsonStringLocalizerFactory(LocalizationCache cache, IServiceProvider serviceProvider) + { + _cache = cache; + _serviceProvider = serviceProvider; + } + + public IStringLocalizer Create(Type resourceSource) + { + return new JsonStringLocalizer(_cache, resourceSource.Name); + } + + public IStringLocalizer Create(string baseName, string location) + { + return new JsonStringLocalizer(_cache, baseName); + } +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/LocalizationCache.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/LocalizationCache.cs new file mode 100644 index 00000000..752db403 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Localization/LocalizationCache.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Net.Http.Json; + +namespace EggymonLandingPage.Client.Localization; + +/// +/// EN: Cache for localization strings loaded from JSON files. +/// VI: Cache cho các chuỗi bản địa hóa được tải từ file JSON. +/// +public class LocalizationCache +{ + private readonly HttpClient _httpClient; + private Dictionary _strings = new(); + private bool _isLoaded; + + public LocalizationCache(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public string? GetString(string key) + { + if (_strings.TryGetValue(key, out var value)) + { + return value; + } + return null; + } + + public async Task LoadAsync(CultureInfo culture) + { + if (_isLoaded) return; + + try + { + var cultureName = culture.Name; + // EN: Map generic culture codes to specific ones + // VI: Map mã ngôn ngữ chung sang mã cụ thể + if (cultureName == "vi") cultureName = "vi-VN"; + if (cultureName == "en") cultureName = "en-US"; + + var loaded = await _httpClient.GetFromJsonAsync>($"/locales/{cultureName}.json?v={DateTime.Now.Ticks}"); + if (loaded != null) + { + _strings = loaded; + _isLoaded = true; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading localization for {culture.Name}: {ex.Message}"); + } + } +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Pages/Home.razor b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Pages/Home.razor new file mode 100644 index 00000000..449398a5 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Pages/Home.razor @@ -0,0 +1,193 @@ +@page "/" +@inject IStringLocalizer L + +EggyMon Kitchen - @L["Hero_Headline"] + + +
+
+
@L["Hero_Badge"]
+

@L["Hero_Headline"]

+

@L["Hero_Subline"]

+ +
+
+ EggyMon Kitchen Store +
+
+ + +
+
+ +

@L["Features_Title"]

+

@L["Features_Desc"]

+
+ +
+ +
+
+ +
+

@L["Feature1_Title"]

+

@L["Feature1_Desc"]

+
+ + +
+
+ +
+

@L["Feature2_Title"]

+

@L["Feature2_Desc"]

+
+ + +
+
+ +
+

@L["Feature3_Title"]

+

@L["Feature3_Desc"]

+
+
+
+ + + + + +
+
+ +

@L["Testimonials_Title"]

+
+ +
+
+

"@L["Testimonial1_Quote"]"

+
+
+

"@L["Testimonial2_Quote"]"

+
+
+

"@L["Testimonial3_Quote"]"

+
+
+
+ + +
+

@L["CTA_Title"]

+

@L["CTA_Subtitle"]

+
+ + +
+
+ + + diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Program.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Program.cs new file mode 100644 index 00000000..7ea8656f --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/Program.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using EggymonLandingPage.Client; +using EggymonLandingPage.Client.Localization; +using Microsoft.Extensions.Localization; +using System.Globalization; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +// EN: Add HttpClient for API calls +// VI: Thêm HttpClient cho các cuộc gọi API +builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress).GetLeftPart(UriPartial.Authority)) }); + +// EN: Add MudBlazor services +// VI: Thêm các services của MudBlazor +builder.Services.AddMudServices(); + +// EN: Add Localization services +// VI: Thêm services đa ngôn ngữ +builder.Services.AddLocalization(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// EN: Build the host +// VI: Build host +var host = builder.Build(); + +// EN: Initialize Localization Cache +// VI: Khởi tạo Localization Cache +var cache = host.Services.GetRequiredService(); + +// EN: Detect culture from BaseAddress (set by from Server) +// VI: Phát hiện ngôn ngữ từ BaseAddress (được set bởi từ Server) +var baseAddress = builder.HostEnvironment.BaseAddress; +var culture = new CultureInfo("en-US"); // Default + +if (baseAddress.Contains("/vi-VN/", StringComparison.OrdinalIgnoreCase)) +{ + culture = new CultureInfo("vi-VN"); +} +else if (baseAddress.Contains("/vi/", StringComparison.OrdinalIgnoreCase)) +{ + culture = new CultureInfo("vi-VN"); +} + +CultureInfo.DefaultThreadCurrentCulture = culture; +CultureInfo.DefaultThreadCurrentUICulture = culture; + +await cache.LoadAsync(culture); + +await host.RunAsync(); diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/_Imports.razor b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/_Imports.razor new file mode 100644 index 00000000..1e970214 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using MudBlazor +@using EggymonLandingPage.Client +@using EggymonLandingPage.Client.Layout +@using EggymonLandingPage.Shared +@using EggymonLandingPage.Client.Components +@using Microsoft.Extensions.Localization diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/css/app.css b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/css/app.css new file mode 100644 index 00000000..22e39638 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/css/app.css @@ -0,0 +1,766 @@ +/* ═══════════════════════════════════════════════════════════════════ + EGGYMON KITCHEN — Design System (matching Pencil design) + Font: Poppins | Colors: #6B4423 (brown), #FF6B35 (orange), + #FAF8F4 (cream), #2C2C2C (dark), #FFFFFF (white) + ═══════════════════════════════════════════════════════════════════ */ + +/* ─── Reset & Base ─── */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + /* EN: Brand Colors / VI: Màu thương hiệu */ + --brown: #6B4423; + --orange: #FF6B35; + --orange-light: #FF6B3520; + --cream: #FAF8F4; + --dark: #2C2C2C; + --gray: #6B6B6B; + --gray-light: #9A9A9A; + --white: #FFFFFF; + --white-dim: #FFFFFF99; + --white-soft: #FFFFFFDD; + --white-glass: #FFFFFF10; + --white-glass2: #FFFFFF20; + --divider-dark: #4A4A4A; + + /* EN: Typography / VI: Kiểu chữ */ + --font: 'Poppins', sans-serif; + + /* EN: Spacing / VI: Khoảng cách */ + --radius-sm: 20px; + --radius-md: 24px; + --radius-lg: 30px; +} + +html, +body { + font-family: var(--font); + color: var(--dark); + background: var(--cream); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; +} + +a { + text-decoration: none; + color: inherit; +} + +img { + max-width: 100%; + display: block; +} + +/* ─── MudBlazor Overrides ─── */ +.mud-appbar { + background: var(--white) !important; + box-shadow: none !important; + border-bottom: 1px solid #eee; +} + +.mud-toolbar { + min-height: 80px !important; + padding: 0 80px !important; +} + +.mud-main-content { + padding-top: 80px; +} + +/* ═══════════════════════════════════════════════════════════════════ + HEADER — Desktop nav with links + Order Now CTA + ═══════════════════════════════════════════════════════════════════ */ +.header-logo { + display: flex; + align-items: center; + gap: 12px; +} + +.header-logo img { + width: 48px; + height: 48px; + object-fit: contain; +} + +.header-logo-text { + font-size: 24px; + font-weight: 700; + color: var(--brown); +} + +.header-nav { + display: flex; + align-items: center; + gap: 40px; +} + +.header-nav a { + font-size: 16px; + font-weight: 500; + color: var(--dark); + transition: color 0.2s; +} + +.header-nav a:hover { + color: var(--orange); +} + +.header-cta { + display: flex; + align-items: center; + gap: 16px; +} + +.btn-order-now { + display: inline-flex; + align-items: center; + padding: 14px 28px; + background: var(--orange); + color: var(--white); + font-family: var(--font); + font-size: 16px; + font-weight: 600; + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.btn-order-now:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(255, 107, 53, 0.3); +} + +/* ═══════════════════════════════════════════════════════════════════ + HERO — Dark brown bg, side-by-side layout + ═══════════════════════════════════════════════════════════════════ */ +.hero-section { + display: flex; + align-items: center; + gap: 60px; + padding: 80px; + background: var(--brown); + min-height: 680px; +} + +.hero-content { + display: flex; + flex-direction: column; + gap: 24px; + max-width: 600px; + flex-shrink: 0; +} + +.hero-badge { + display: inline-flex; + align-self: flex-start; + padding: 8px 16px; + background: var(--orange); + color: var(--white); + font-size: 14px; + font-weight: 600; + border-radius: 20px; +} + +.hero-headline { + font-size: 64px; + font-weight: 700; + color: var(--white); + line-height: 1.1; +} + +.hero-subline { + font-size: 20px; + font-weight: 400; + color: var(--white-dim); + line-height: 1.6; +} + +.hero-cta-row { + display: flex; + gap: 16px; +} + +.hero-btn-primary { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 18px 36px; + background: var(--orange); + color: var(--white); + font-family: var(--font); + font-size: 16px; + font-weight: 600; + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: transform 0.2s; +} + +.hero-btn-primary:hover { + transform: translateY(-2px); +} + +.hero-btn-secondary { + display: inline-flex; + align-items: center; + padding: 18px 36px; + background: transparent; + color: var(--white); + font-family: var(--font); + font-size: 16px; + font-weight: 600; + border: 2px solid var(--white); + border-radius: var(--radius-lg); + cursor: pointer; + transition: background 0.2s; +} + +.hero-btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); +} + +.hero-image { + flex: 1; + min-width: 400px; + height: 520px; + border-radius: var(--radius-md); + overflow: hidden; +} + +.hero-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* ═══════════════════════════════════════════════════════════════════ + SECTIONS — Common patterns + ═══════════════════════════════════════════════════════════════════ */ +.section-tag { + font-size: 14px; + font-weight: 600; + color: var(--orange); + letter-spacing: 2px; + text-transform: uppercase; +} + +.section-title { + font-size: 48px; + font-weight: 700; + color: var(--dark); + line-height: 1.2; +} + +.section-title-white { + font-size: 48px; + font-weight: 700; + color: var(--white); + line-height: 1.2; +} + +.section-desc { + font-size: 18px; + font-weight: 400; + color: var(--gray); +} + +.section-desc-white { + font-size: 18px; + font-weight: 400; + color: var(--white-dim); +} + +/* ═══════════════════════════════════════════════════════════════════ + FEATURES — 3 cards, white bg + ═══════════════════════════════════════════════════════════════════ */ +.features-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; + padding: 80px 120px; + background: var(--white); +} + +.features-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + text-align: center; +} + +.features-grid { + display: flex; + gap: 32px; + justify-content: center; +} + +.feature-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + padding: 32px; + background: var(--cream); + border-radius: var(--radius-md); + width: 320px; + text-align: center; +} + +.feature-icon { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + background: var(--orange-light); + border-radius: 32px; +} + +.feature-icon .mud-icon-root { + color: var(--orange) !important; +} + +.feature-title { + font-size: 22px; + font-weight: 600; + color: var(--dark); +} + +.feature-desc { + font-size: 15px; + font-weight: 400; + color: var(--gray); + line-height: 1.6; +} + +/* ═══════════════════════════════════════════════════════════════════ + MENU — Dark brown bg, 3 text columns with dividers + ═══════════════════════════════════════════════════════════════════ */ +.menu-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; + padding: 80px 120px; + background: var(--brown); +} + +.menu-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + text-align: center; +} + +.menu-grid { + display: flex; + gap: 40px; + justify-content: center; + width: 100%; + padding: 32px 40px; + background: var(--white-glass); + border-radius: var(--radius-sm); +} + +.menu-column { + display: flex; + flex-direction: column; + gap: 14px; + flex: 1; +} + +.menu-column-title { + font-size: 16px; + font-weight: 700; + color: var(--orange); + letter-spacing: 3px; +} + +.menu-divider-line { + width: 40px; + height: 2px; + background: var(--orange); +} + +.menu-item { + font-size: 15px; + font-weight: 500; + color: var(--white-soft); +} + +.menu-column-separator { + width: 1px; + background: var(--white-glass2); + align-self: stretch; +} + +/* ═══════════════════════════════════════════════════════════════════ + TESTIMONIALS — Cream bg, 3 quote cards (no stars, no avatars) + ═══════════════════════════════════════════════════════════════════ */ +.testimonials-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 48px; + padding: 80px 120px; + background: var(--cream); +} + +.testimonials-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + text-align: center; +} + +.testimonials-grid { + display: flex; + gap: 32px; +} + +.testimonial-card { + display: flex; + flex-direction: column; + gap: 20px; + padding: 32px; + background: var(--white); + border-radius: var(--radius-md); + width: 380px; +} + +.testimonial-quote { + font-size: 16px; + font-weight: 400; + color: var(--dark); + line-height: 1.7; + font-style: italic; +} + +/* ═══════════════════════════════════════════════════════════════════ + CTA — Orange bg, 2 buttons + ═══════════════════════════════════════════════════════════════════ */ +.cta-section { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 32px; + padding: 80px 120px; + background: var(--orange); + min-height: 400px; + text-align: center; +} + +.cta-title { + font-size: 48px; + font-weight: 700; + color: var(--white); +} + +.cta-subtitle { + font-size: 20px; + font-weight: 400; + color: var(--white-dim); +} + +.cta-btn-row { + display: flex; + gap: 16px; +} + +.cta-btn-primary { + padding: 18px 40px; + background: var(--white); + color: var(--orange); + font-family: var(--font); + font-size: 18px; + font-weight: 600; + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: transform 0.2s; +} + +.cta-btn-primary:hover { + transform: translateY(-2px); +} + +.cta-btn-secondary { + padding: 18px 40px; + background: transparent; + color: var(--white); + font-family: var(--font); + font-size: 18px; + font-weight: 600; + border: 2px solid var(--white); + border-radius: var(--radius-lg); + cursor: pointer; + transition: background 0.2s; +} + +.cta-btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* ═══════════════════════════════════════════════════════════════════ + FOOTER — Dark bg, logo + 3 link columns + divider + legal row + ═══════════════════════════════════════════════════════════════════ */ +.footer { + display: flex; + flex-direction: column; + gap: 40px; + padding: 60px 120px; + background: var(--dark); +} + +.footer-top { + display: flex; + justify-content: space-between; + width: 100%; +} + +.footer-brand { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 320px; +} + +.footer-logo-row { + display: flex; + align-items: center; + gap: 12px; +} + +.footer-logo-row img { + width: 36px; + height: 36px; +} + +.footer-logo-text { + font-size: 20px; + font-weight: 700; + color: var(--white); +} + +.footer-tagline { + font-size: 14px; + font-weight: 400; + color: var(--gray-light); + line-height: 1.6; +} + +.footer-links { + display: flex; + gap: 80px; +} + +.footer-col { + display: flex; + flex-direction: column; + gap: 16px; +} + +.footer-col-title { + font-size: 16px; + font-weight: 600; + color: var(--white); +} + +.footer-col a, +.footer-col span { + font-size: 14px; + font-weight: 400; + color: var(--gray-light); + transition: color 0.2s; +} + +.footer-col a:hover { + color: var(--white); +} + +.footer-divider { + width: 100%; + height: 1px; + background: var(--divider-dark); +} + +.footer-bottom { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.footer-copyright { + font-size: 14px; + font-weight: 400; + color: var(--gray); +} + +.footer-legal { + display: flex; + gap: 24px; +} + +.footer-legal a { + font-size: 14px; + font-weight: 400; + color: var(--gray); + transition: color 0.2s; +} + +.footer-legal a:hover { + color: var(--white); +} + +/* ═══════════════════════════════════════════════════════════════════ + RESPONSIVE + ═══════════════════════════════════════════════════════════════════ */ +@media (max-width: 1200px) { + .hero-section { + flex-direction: column; + padding: 60px 40px; + } + + .hero-image { + min-width: 100%; + height: 400px; + } + + .hero-headline { + font-size: 48px; + } + + .features-section, + .menu-section, + .testimonials-section, + .cta-section { + padding: 60px 40px; + } + + .footer { + padding: 40px; + } + + .footer-links { + gap: 40px; + } + + .mud-toolbar { + padding: 0 40px !important; + } +} + +@media (max-width: 768px) { + .hero-section { + padding: 40px 20px; + min-height: auto; + } + + .hero-headline { + font-size: 36px; + } + + .hero-subline { + font-size: 16px; + } + + .hero-image { + min-width: 100%; + height: 300px; + } + + .hero-cta-row { + flex-direction: column; + } + + .features-grid { + flex-direction: column; + align-items: center; + } + + .feature-card { + width: 100%; + max-width: 400px; + } + + .menu-grid { + flex-direction: column; + } + + .menu-column-separator { + width: 100%; + height: 1px; + } + + .testimonials-grid { + flex-direction: column; + align-items: center; + } + + .testimonial-card { + width: 100%; + max-width: 400px; + } + + .section-title, + .section-title-white, + .cta-title { + font-size: 32px; + } + + .footer-top { + flex-direction: column; + gap: 40px; + } + + .footer-links { + flex-direction: column; + gap: 24px; + } + + .footer-bottom { + flex-direction: column; + gap: 12px; + text-align: center; + } + + .header-nav { + display: none; + } + + .mud-toolbar { + padding: 0 20px !important; + } + + .features-section, + .menu-section, + .testimonials-section, + .cta-section { + padding: 40px 20px; + } + + .footer { + padding: 30px 20px; + } +} + +/* ─── Animation ─── */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/Logo-Eggymon-Kitchen.png b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/Logo-Eggymon-Kitchen.png new file mode 100644 index 00000000..26e4059a Binary files /dev/null and b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/Logo-Eggymon-Kitchen.png differ diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/eggymon-kitchen-store.png b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/eggymon-kitchen-store.png new file mode 100644 index 00000000..880f5f8e Binary files /dev/null and b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/images/eggymon-kitchen-store.png differ diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/index.html b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/index.html new file mode 100644 index 00000000..fda9dc93 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/index.html @@ -0,0 +1,100 @@ + + + + + + + Eggymon Kitchen - Fresh Eggs, Happy Kitchen + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + +

Loading + Eggymon Kitchen...

+
+ + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/en-US.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/en-US.json new file mode 100644 index 00000000..dcea1c92 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/en-US.json @@ -0,0 +1,68 @@ +{ + "Nav_Menu": "Menu", + "Nav_About": "About Us", + "Nav_Locations": "Locations", + "Nav_Contact": "Contact", + "Nav_OrderNow": "Order Now", + "Hero_Badge": "🥚 Open 24/7 — Fresh & Delicious", + "Hero_Headline": "Egg-cellent Food,\nMade With Love", + "Hero_Subline": "Welcome to EggyMon Kitchen — your neighborhood comfort food destination. From fluffy omelettes to savory rice bowls, we serve delicious egg-based dishes around the clock.", + "Hero_CTA_Primary": "Order Now", + "Hero_CTA_Secondary": "Learn More", + "Features_Tag": "WHY CHOOSE US", + "Features_Title": "Crafted With Care, Served With Soul", + "Features_Desc": "Every dish at EggyMon Kitchen is prepared fresh with premium ingredients", + "Feature1_Title": "Open 24/7", + "Feature1_Desc": "Craving eggs at 3 AM? We've got you covered. Fresh food, any time of day.", + "Feature2_Title": "Farm Fresh Eggs", + "Feature2_Desc": "We source our eggs from local farms daily for the freshest taste in every bite.", + "Feature3_Title": "Made With Love", + "Feature3_Desc": "Our chefs pour passion into every dish. Taste the difference of home-cooked comfort.", + "Menu_Tag": "OUR MENU", + "Menu_Title": "Our Menu", + "Menu_Desc": "Explore our full range of dishes — from comfort food classics to Vietnamese specialties", + "Menu_Col1_Title": "MÓN CHÍNH", + "Menu_Col1_Item1": "Cheese Foam Milk Tea", + "Menu_Col1_Item2": "Strawberry Milk Tea", + "Menu_Col1_Item3": "Oolong Milk Tea", + "Menu_Col1_Item4": "Bacon Pork Cheeseburger", + "Menu_Col1_Item5": "Crispy Chicken Burger", + "Menu_Col1_Item6": "Beef Burger with Cheese", + "Menu_Col2_Title": "MÓN PHỤ — ĂN VẶT", + "Menu_Col2_Item1": "Salad trộn", + "Menu_Col2_Item2": "Khoai Tây Chiên", + "Menu_Col2_Item3": "Bánh Tráng Trộn", + "Menu_Col2_Item4": "Bánh Lăng", + "Menu_Col2_Item5": "Mực rim me", + "Menu_Col2_Item6": "Chè đậu Xanh", + "Menu_Col2_Item7": "Chè Thập Cẩm", + "Menu_Col3_Title": "ĐẶC BIỆT", + "Menu_Col3_Item1": "Phở Việt", + "Menu_Col3_Item2": "Hủ Tiếu", + "Testimonials_Tag": "TESTIMONIALS", + "Testimonials_Title": "What Our Customers Say", + "Testimonial1_Quote": "Best omelette I've ever had! The eggs are so fresh and fluffy. Now I'm a regular — even at 2 AM!", + "Testimonial2_Quote": "My family's go-to spot for late night cravings. The staff is friendly and the food is always consistent.", + "Testimonial3_Quote": "The shakshuka here reminds me of home. Authentic flavors, generous portions, and the price is right!", + "CTA_Title": "Ready to Try the Best Eggs in Town?", + "CTA_Subtitle": "Visit us today or order online for delivery. Open 24/7 for your convenience!", + "CTA_Btn_Primary": "Order Now", + "CTA_Btn_Secondary": "Find Location", + "Footer_Tagline": "Your neighborhood comfort food destination. Serving egg-cellent dishes 24/7.", + "Footer_Col1_Title": "Company", + "Footer_Col1_Link1": "About Us", + "Footer_Col1_Link2": "Our Menu", + "Footer_Col1_Link3": "Testimonials", + "Footer_Col1_Link4": "Contact", + "Footer_Col2_Title": "Support", + "Footer_Col2_Link1": "Help Center", + "Footer_Col2_Link2": "Order Tracking", + "Footer_Col2_Link3": "Delivery Areas", + "Footer_Col3_Title": "Connect", + "Footer_Col3_Link1": "Facebook", + "Footer_Col3_Link2": "Instagram", + "Footer_Col3_Link3": "TikTok", + "Footer_Copyright": "© 2026 EggyMon Kitchen. All rights reserved.", + "Footer_Privacy": "Privacy Policy", + "Footer_Terms": "Terms of Service" +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/vi-VN.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/vi-VN.json new file mode 100644 index 00000000..d4c7e10f --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Client/wwwroot/locales/vi-VN.json @@ -0,0 +1,68 @@ +{ + "Nav_Menu": "Thực đơn", + "Nav_About": "Về chúng tôi", + "Nav_Locations": "Chi nhánh", + "Nav_Contact": "Liên hệ", + "Nav_OrderNow": "Đặt Ngay", + "Hero_Badge": "🥚 Mở 24/7 — Tươi Ngon & Hấp Dẫn", + "Hero_Headline": "Món Trứng Tuyệt Vời,\nNấu Với Tình Yêu", + "Hero_Subline": "Chào mừng đến EggyMon Kitchen — quán ăn quen thuộc của bạn. Từ trứng chiên mềm mịn đến cơm rang thơm ngon, chúng tôi phục vụ 24/7.", + "Hero_CTA_Primary": "Đặt Ngay", + "Hero_CTA_Secondary": "Tìm Hiểu Thêm", + "Features_Tag": "TẠI SAO CHỌN CHÚNG TÔI", + "Features_Title": "Chế Biến Tỉ Mỉ, Phục Vụ Tận Tâm", + "Features_Desc": "Mỗi món ăn tại EggyMon Kitchen đều được chế biến tươi với nguyên liệu cao cấp", + "Feature1_Title": "Mở 24/7", + "Feature1_Desc": "Thèm trứng lúc 3 giờ sáng? Chúng tôi sẵn sàng phục vụ bạn bất cứ lúc nào.", + "Feature2_Title": "Trứng Tươi Từ Trang Trại", + "Feature2_Desc": "Chúng tôi nhập trứng từ trang trại địa phương mỗi ngày để đảm bảo vị tươi ngon nhất.", + "Feature3_Title": "Nấu Với Tình Yêu", + "Feature3_Desc": "Đầu bếp của chúng tôi đổ đam mê vào mỗi món ăn. Hãy nếm thử sự khác biệt.", + "Menu_Tag": "THỰC ĐƠN", + "Menu_Title": "Thực Đơn", + "Menu_Desc": "Khám phá đầy đủ các món — từ comfort food kinh điển đến đặc sản Việt Nam", + "Menu_Col1_Title": "MÓN CHÍNH", + "Menu_Col1_Item1": "Cheese Foam Milk Tea", + "Menu_Col1_Item2": "Strawberry Milk Tea", + "Menu_Col1_Item3": "Oolong Milk Tea", + "Menu_Col1_Item4": "Bacon Pork Cheeseburger", + "Menu_Col1_Item5": "Crispy Chicken Burger", + "Menu_Col1_Item6": "Beef Burger with Cheese", + "Menu_Col2_Title": "MÓN PHỤ — ĂN VẶT", + "Menu_Col2_Item1": "Salad trộn", + "Menu_Col2_Item2": "Khoai Tây Chiên", + "Menu_Col2_Item3": "Bánh Tráng Trộn", + "Menu_Col2_Item4": "Bánh Lăng", + "Menu_Col2_Item5": "Mực rim me", + "Menu_Col2_Item6": "Chè đậu Xanh", + "Menu_Col2_Item7": "Chè Thập Cẩm", + "Menu_Col3_Title": "ĐẶC BIỆT", + "Menu_Col3_Item1": "Phở Việt", + "Menu_Col3_Item2": "Hủ Tiếu", + "Testimonials_Tag": "ĐÁNH GIÁ", + "Testimonials_Title": "Khách Hàng Nói Gì?", + "Testimonial1_Quote": "Trứng chiên ngon nhất mà tôi từng ăn! Trứng rất tươi và xốp. Giờ tôi là khách quen — kể cả lúc 2 giờ sáng!", + "Testimonial2_Quote": "Địa điểm yêu thích của gia đình tôi cho những cơn thèm đêm khuya. Nhân viên thân thiện và đồ ăn luôn ổn định.", + "Testimonial3_Quote": "Món shakshuka ở đây gợi nhớ hương vị quê nhà. Vị đậm đà, phần ăn hào phóng, giá cả hợp lý!", + "CTA_Title": "Sẵn Sàng Thử Trứng Ngon Nhất Thành Phố?", + "CTA_Subtitle": "Ghé thăm chúng tôi hôm nay hoặc đặt hàng trực tuyến. Mở 24/7 để phục vụ bạn!", + "CTA_Btn_Primary": "Đặt Ngay", + "CTA_Btn_Secondary": "Tìm Chi Nhánh", + "Footer_Tagline": "Quán ăn quen thuộc phục vụ món trứng tuyệt vời 24/7.", + "Footer_Col1_Title": "Công Ty", + "Footer_Col1_Link1": "Về Chúng Tôi", + "Footer_Col1_Link2": "Thực Đơn", + "Footer_Col1_Link3": "Đánh Giá", + "Footer_Col1_Link4": "Liên Hệ", + "Footer_Col2_Title": "Hỗ Trợ", + "Footer_Col2_Link1": "Trung Tâm Trợ Giúp", + "Footer_Col2_Link2": "Theo Dõi Đơn Hàng", + "Footer_Col2_Link3": "Khu Vực Giao Hàng", + "Footer_Col3_Title": "Kết Nối", + "Footer_Col3_Link1": "Facebook", + "Footer_Col3_Link2": "Instagram", + "Footer_Col3_Link3": "TikTok", + "Footer_Copyright": "© 2026 EggyMon Kitchen. Bảo lưu mọi quyền.", + "Footer_Privacy": "Chính Sách Bảo Mật", + "Footer_Terms": "Điều Khoản Dịch Vụ" +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj new file mode 100644 index 00000000..a9ed57c7 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Program.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Program.cs new file mode 100644 index 00000000..958cd723 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Program.cs @@ -0,0 +1,117 @@ +/// +/// EN: ASP.NET Core BFF (Backend for Frontend) with YARP Reverse Proxy for Eggymon Landing Page. +/// VI: ASP.NET Core BFF (Backend for Frontend) với YARP Reverse Proxy cho Eggymon Landing Page. +/// + +using Microsoft.AspNetCore.Rewrite; + +var builder = WebApplication.CreateBuilder(args); + +// ═══════════════════════════════════════════════════════════════════════════════ +// EN: Add services to the container +// VI: Thêm các services vào container +// ═══════════════════════════════════════════════════════════════════════════════ + +// EN: Load YARP configuration from yarp.json +// VI: Load cấu hình YARP từ yarp.json +builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true); + +// EN: Add YARP Reverse Proxy +// VI: Thêm YARP Reverse Proxy +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + +// EN: Add OpenAPI/Swagger support +// VI: Thêm hỗ trợ OpenAPI/Swagger +builder.Services.AddOpenApi(); + +// EN: Add CORS for Blazor WebAssembly client +// VI: Thêm CORS cho Blazor WebAssembly client +builder.Services.AddCors(options => +{ + options.AddPolicy("BlazorClient", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +// EN: Add health checks +// VI: Thêm health checks +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// ═══════════════════════════════════════════════════════════════════════════════ +// EN: Configure the HTTP request pipeline +// VI: Cấu hình HTTP request pipeline +// ═══════════════════════════════════════════════════════════════════════════════ + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.UseDeveloperExceptionPage(); +} + +app.UseHttpsRedirection(); + +// EN: Enable CORS +// VI: Kích hoạt CORS +app.UseCors("BlazorClient"); + +// EN: Rewrite localized framework/content requests to root +// VI: Viết lại các yêu cầu framework/content từ đường dẫn ngôn ngữ về root +var rewriteOptions = new RewriteOptions() + .AddRewrite(@"^(en-US|vi-VN)/(_framework|_content)/(.*)", "$2/$3", skipRemainingRules: true); +app.UseRewriter(rewriteOptions); + +// EN: Serve static files with fingerprinting support (.NET 10+) +// VI: Phục vụ static files với hỗ trợ fingerprinting (.NET 10+) +app.MapStaticAssets(); + +// EN: Map health check endpoint +// VI: Map endpoint health check +app.MapHealthChecks("/health"); + +// EN: Map YARP Reverse Proxy routes to microservices +// VI: Map các routes YARP Reverse Proxy đến microservices +app.MapReverseProxy(); + +// EN: Localization Support - Serve index.html with dynamic base tag for specific cultures +// VI: Hỗ trợ đa ngôn ngữ - Phục vụ index.html với base tag động cho các ngôn ngữ cụ thể +var supportedCultures = new[] { "en-US", "vi-VN" }; +var localizationOptions = new RequestLocalizationOptions() + .SetDefaultCulture("en-US") + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures); + +app.UseRequestLocalization(localizationOptions); + +// EN: Handle mapped culture routes (e.g. /en-US/home, /vi-VN/) +// VI: Xử lý các routes ngôn ngữ (vd: /en-US/home, /vi-VN/) +app.Map("{culture:regex(^(en-US|vi-VN)$)}/{**slug}", async (string culture, HttpContext context, IWebHostEnvironment env) => +{ + var fileInfo = env.WebRootFileProvider.GetFileInfo("index.html"); + if (!fileInfo.Exists) + { + return Results.NotFound("index.html not found in wwwroot. Ensure the Client project is built."); + } + + using var stream = fileInfo.CreateReadStream(); + using var reader = new StreamReader(stream); + var html = await reader.ReadToEndAsync(); + + // EN: Replace base tag for culture-specific routing + // VI: Thay thế base tag cho routing theo ngôn ngữ + var modifiedHtml = html.Replace("", $"") + .Replace("", $""); + + return Results.Content(modifiedHtml, "text/html"); +}); + +// EN: Fallback to index.html for SPA routing (default culture) +// VI: Fallback đến index.html cho SPA routing (ngôn ngữ mặc định) +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Properties/launchSettings.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Properties/launchSettings.json new file mode 100644 index 00000000..22b22cc3 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7295;http://localhost:5095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.Development.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.Development.json new file mode 100644 index 00000000..fc52b051 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.json new file mode 100644 index 00000000..5220ddea --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/yarp.json b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/yarp.json new file mode 100644 index 00000000..c4876b3b --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Server/yarp.json @@ -0,0 +1,6 @@ +{ + "ReverseProxy": { + "Routes": {}, + "Clusters": {} + } +} \ No newline at end of file diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/ApiResponse.cs b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/ApiResponse.cs new file mode 100644 index 00000000..386cf124 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/ApiResponse.cs @@ -0,0 +1,12 @@ +namespace EggymonLandingPage.Shared; + +/// +/// EN: Standard API response wrapper. +/// VI: Wrapper response API chuẩn. +/// +public class ApiResponse +{ + public bool Success { get; set; } + public T? Data { get; set; } + public string? Error { get; set; } +} diff --git a/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/EggymonLandingPage.Shared.csproj b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/EggymonLandingPage.Shared.csproj new file mode 100644 index 00000000..93f5ab4b --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/src/EggymonLandingPage.Shared/EggymonLandingPage.Shared.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + +