feat: add en-US localization file and generated build artifacts for client and server projects

This commit is contained in:
Ho Ngoc Hai
2026-02-09 00:22:25 +07:00
parent d55fcdddbe
commit 3653a1ca79
27 changed files with 1896 additions and 0 deletions

View File

@@ -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"]

View File

@@ -0,0 +1,7 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/EggymonLandingPage.Client/EggymonLandingPage.Client.csproj" />
<Project Path="src/EggymonLandingPage.Server/EggymonLandingPage.Server.csproj" />
<Project Path="src/EggymonLandingPage.Shared/EggymonLandingPage.Shared.csproj" />
</Folder>
</Solution>

View File

@@ -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

View File

@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not Found</PageTitle>
<LayoutView Layout="@typeof(Layout.MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,66 @@
@using System.Globalization
@inject NavigationManager Navigation
<MudMenu Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight" LockScroll="true">
<ActivatorContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mr-2 cursor-pointer">
<MudText Typo="Typo.button" Style="font-family: var(--font-heading);">
@GetCurrentLabel()
</MudText>
<MudIcon Icon="@Icons.Material.Rounded.Language" Size="Size.Small" />
</MudStack>
</ActivatorContent>
<ChildContent>
<MudMenuItem OnClick="@(() => SwitchLanguage("vi-VN"))">
<MudStack Row="true" Spacing="2">
<MudText>🇻🇳</MudText>
<MudText>Tiếng Việt</MudText>
</MudStack>
</MudMenuItem>
<MudMenuItem OnClick="@(() => SwitchLanguage("en-US"))">
<MudStack Row="true" Spacing="2">
<MudText>🇺🇸</MudText>
<MudText>English</MudText>
</MudStack>
</MudMenuItem>
</ChildContent>
</MudMenu>
@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);
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" />
<PackageReference Include="MudBlazor" Version="8.15.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EggymonLandingPage.Shared\EggymonLandingPage.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,62 @@
@inherits LayoutComponentBase
@inject IStringLocalizer<MainLayout> L
<MudThemeProvider @bind-IsDarkMode="@_isDarkMode" Theme="_theme" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar Elevation="0" Fixed="true" Color="Color.Transparent">
<!-- EN: App Header — Logo + Nav Links + Order Now CTA -->
<!-- VI: Header — Logo + Liên kết + Nút Order Now -->
<div class="header-logo">
<img src="/images/Logo-Eggymon-Kitchen.png" alt="EggyMon Kitchen" />
<span class="header-logo-text">EggyMon Kitchen</span>
</div>
<MudSpacer />
<!-- EN: Navigation Links / VI: Liên kết điều hướng -->
<nav class="header-nav">
<a href="#menu">@L["Nav_Menu"]</a>
<a href="#about">@L["Nav_About"]</a>
<a href="#locations">@L["Nav_Locations"]</a>
<a href="#contact">@L["Nav_Contact"]</a>
</nav>
<MudSpacer />
<!-- EN: CTA Area / VI: Khu vực CTA -->
<div class="header-cta">
<LanguageSwitcher />
<a href="#menu" class="btn-order-now">@L["Nav_OrderNow"]</a>
</div>
</MudAppBar>
<MudMainContent>
@Body
</MudMainContent>
</MudLayout>
@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"
},
};
}

View File

@@ -0,0 +1,56 @@
using System.Globalization;
using System.Net.Http.Json;
using Microsoft.Extensions.Localization;
namespace EggymonLandingPage.Client.Localization;
/// <summary>
/// EN: JSON-based string localizer for Blazor WASM.
/// VI: String localizer dựa trên JSON cho Blazor WASM.
/// </summary>
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<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return Enumerable.Empty<LocalizedString>();
}
private string? GetString(string name)
{
return _cache.GetString(name);
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.Extensions.Localization;
namespace EggymonLandingPage.Client.Localization;
/// <summary>
/// EN: Factory for creating JSON string localizers.
/// VI: Factory tạo JSON string localizer.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,54 @@
using System.Globalization;
using System.Net.Http.Json;
namespace EggymonLandingPage.Client.Localization;
/// <summary>
/// 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.
/// </summary>
public class LocalizationCache
{
private readonly HttpClient _httpClient;
private Dictionary<string, string> _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<Dictionary<string, string>>($"/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}");
}
}
}

View File

@@ -0,0 +1,193 @@
@page "/"
@inject IStringLocalizer<Home> L
<PageTitle>EggyMon Kitchen - @L["Hero_Headline"]</PageTitle>
<!-- ═══════════════════════════════════════════════════════════════════
1. HERO SECTION — Dark brown bg, side-by-side layout
═══════════════════════════════════════════════════════════════════ -->
<section class="hero-section">
<div class="hero-content">
<div class="hero-badge">@L["Hero_Badge"]</div>
<h1 class="hero-headline">@L["Hero_Headline"]</h1>
<p class="hero-subline">@L["Hero_Subline"]</p>
<div class="hero-cta-row">
<a href="#menu" class="hero-btn-primary">@L["Hero_CTA_Primary"]</a>
<a href="#testimonials" class="hero-btn-secondary">@L["Hero_CTA_Secondary"]</a>
</div>
</div>
<div class="hero-image">
<img src="/images/eggymon-kitchen-store.png" alt="EggyMon Kitchen Store" />
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════
2. FEATURES SECTION — White bg, 3 cards
═══════════════════════════════════════════════════════════════════ -->
<section class="features-section">
<div class="features-header">
<span class="section-tag">@L["Features_Tag"]</span>
<h2 class="section-title">@L["Features_Title"]</h2>
<p class="section-desc">@L["Features_Desc"]</p>
</div>
<div class="features-grid">
<!-- EN: Feature 1: Open 24/7 / VI: Mở 24/7 -->
<div class="feature-card">
<div class="feature-icon">
<MudIcon Icon="@Icons.Material.Rounded.Schedule" Size="Size.Large" />
</div>
<h3 class="feature-title">@L["Feature1_Title"]</h3>
<p class="feature-desc">@L["Feature1_Desc"]</p>
</div>
<!-- EN: Feature 2: Farm Fresh Eggs / VI: Trứng Tươi -->
<div class="feature-card">
<div class="feature-icon">
<MudIcon Icon="@Icons.Material.Rounded.Spa" Size="Size.Large" />
</div>
<h3 class="feature-title">@L["Feature2_Title"]</h3>
<p class="feature-desc">@L["Feature2_Desc"]</p>
</div>
<!-- EN: Feature 3: Made With Love / VI: Làm Với Tình Yêu -->
<div class="feature-card">
<div class="feature-icon">
<MudIcon Icon="@Icons.Material.Rounded.Favorite" Size="Size.Large" />
</div>
<h3 class="feature-title">@L["Feature3_Title"]</h3>
<p class="feature-desc">@L["Feature3_Desc"]</p>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════
3. MENU SECTION — Dark brown bg, 3 text columns
═══════════════════════════════════════════════════════════════════ -->
<section class="menu-section" id="menu">
<div class="menu-header">
<span class="section-tag">@L["Menu_Tag"]</span>
<h2 class="section-title-white">@L["Menu_Title"]</h2>
<p class="section-desc-white">@L["Menu_Desc"]</p>
</div>
<div class="menu-grid">
<!-- EN: Column 1: Main Dishes / VI: Cột 1: Món Chính -->
<div class="menu-column">
<span class="menu-column-title">@L["Menu_Col1_Title"]</span>
<div class="menu-divider-line"></div>
<span class="menu-item">@L["Menu_Col1_Item1"]</span>
<span class="menu-item">@L["Menu_Col1_Item2"]</span>
<span class="menu-item">@L["Menu_Col1_Item3"]</span>
<span class="menu-item">@L["Menu_Col1_Item4"]</span>
<span class="menu-item">@L["Menu_Col1_Item5"]</span>
<span class="menu-item">@L["Menu_Col1_Item6"]</span>
</div>
<div class="menu-column-separator"></div>
<!-- EN: Column 2: Sides & Snacks / VI: Cột 2: Món Phụ -->
<div class="menu-column">
<span class="menu-column-title">@L["Menu_Col2_Title"]</span>
<div class="menu-divider-line"></div>
<span class="menu-item">@L["Menu_Col2_Item1"]</span>
<span class="menu-item">@L["Menu_Col2_Item2"]</span>
<span class="menu-item">@L["Menu_Col2_Item3"]</span>
<span class="menu-item">@L["Menu_Col2_Item4"]</span>
<span class="menu-item">@L["Menu_Col2_Item5"]</span>
<span class="menu-item">@L["Menu_Col2_Item6"]</span>
<span class="menu-item">@L["Menu_Col2_Item7"]</span>
</div>
<div class="menu-column-separator"></div>
<!-- EN: Column 3: Specials / VI: Cột 3: Đặc Biệt -->
<div class="menu-column">
<span class="menu-column-title">@L["Menu_Col3_Title"]</span>
<div class="menu-divider-line"></div>
<span class="menu-item">@L["Menu_Col3_Item1"]</span>
<span class="menu-item">@L["Menu_Col3_Item2"]</span>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════
4. TESTIMONIALS SECTION — Cream bg, 3 quote cards
═══════════════════════════════════════════════════════════════════ -->
<section class="testimonials-section" id="testimonials">
<div class="testimonials-header">
<span class="section-tag">@L["Testimonials_Tag"]</span>
<h2 class="section-title">@L["Testimonials_Title"]</h2>
</div>
<div class="testimonials-grid">
<div class="testimonial-card">
<p class="testimonial-quote">"@L["Testimonial1_Quote"]"</p>
</div>
<div class="testimonial-card">
<p class="testimonial-quote">"@L["Testimonial2_Quote"]"</p>
</div>
<div class="testimonial-card">
<p class="testimonial-quote">"@L["Testimonial3_Quote"]"</p>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════
5. CTA SECTION — Orange bg, 2 buttons
═══════════════════════════════════════════════════════════════════ -->
<section class="cta-section">
<h2 class="cta-title">@L["CTA_Title"]</h2>
<p class="cta-subtitle">@L["CTA_Subtitle"]</p>
<div class="cta-btn-row">
<button class="cta-btn-primary">@L["CTA_Btn_Primary"]</button>
<button class="cta-btn-secondary">@L["CTA_Btn_Secondary"]</button>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════
6. FOOTER — Dark bg, logo + 3 link columns + divider + legal
═══════════════════════════════════════════════════════════════════ -->
<footer class="footer" id="contact">
<div class="footer-top">
<div class="footer-brand">
<div class="footer-logo-row">
<img src="/images/Logo-Eggymon-Kitchen.png" alt="EggyMon Kitchen" />
<span class="footer-logo-text">EggyMon Kitchen</span>
</div>
<p class="footer-tagline">@L["Footer_Tagline"]</p>
</div>
<div class="footer-links">
<!-- EN: Column 1: Company / VI: Cột 1: Công ty -->
<div class="footer-col">
<span class="footer-col-title">@L["Footer_Col1_Title"]</span>
<a href="#about">@L["Footer_Col1_Link1"]</a>
<a href="#menu">@L["Footer_Col1_Link2"]</a>
<a href="#testimonials">@L["Footer_Col1_Link3"]</a>
<a href="#contact">@L["Footer_Col1_Link4"]</a>
</div>
<!-- EN: Column 2: Support / VI: Cột 2: Hỗ trợ -->
<div class="footer-col">
<span class="footer-col-title">@L["Footer_Col2_Title"]</span>
<a href="#">@L["Footer_Col2_Link1"]</a>
<a href="#">@L["Footer_Col2_Link2"]</a>
<a href="#">@L["Footer_Col2_Link3"]</a>
</div>
<!-- EN: Column 3: Connect / VI: Cột 3: Kết nối -->
<div class="footer-col">
<span class="footer-col-title">@L["Footer_Col3_Title"]</span>
<a href="#">@L["Footer_Col3_Link1"]</a>
<a href="#">@L["Footer_Col3_Link2"]</a>
<a href="#">@L["Footer_Col3_Link3"]</a>
</div>
</div>
</div>
<div class="footer-divider"></div>
<div class="footer-bottom">
<span class="footer-copyright">@L["Footer_Copyright"]</span>
<div class="footer-legal">
<a href="#">@L["Footer_Privacy"]</a>
<a href="#">@L["Footer_Terms"]</a>
</div>
</div>
</footer>

View File

@@ -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>("#app");
builder.RootComponents.Add<HeadOutlet>("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<LocalizationCache>();
builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
// 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<LocalizationCache>();
// EN: Detect culture from BaseAddress (set by <base href> from Server)
// VI: Phát hiện ngôn ngữ từ BaseAddress (được set bởi <base href> 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();

View File

@@ -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

View File

@@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Eggymon Kitchen - Fresh Eggs, Happy Kitchen</title>
<meta name="description"
content="Eggymon Kitchen - Premium egg dishes crafted with love. Fresh eggs, happy kitchen. Order online now!" />
<base href="/" />
<!-- EN: Google Fonts - Poppins -->
<!-- VI: Google Fonts - Poppins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- EN: Material Design Icons -->
<!-- VI: Icons Material Design -->
<link
href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Two+Tone|Material+Icons+Round|Material+Icons+Sharp"
rel="stylesheet">
<!-- EN: MudBlazor CSS -->
<!-- VI: CSS MudBlazor -->
<link href="/_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<!-- EN: Custom CSS -->
<!-- VI: CSS tùy chỉnh -->
<link rel="stylesheet" href="/css/app.css" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link href="/EggymonLandingPage.Client.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<!-- EN: Loading indicator with Eggymon branding -->
<!-- VI: Chỉ báo đang tải với branding Eggymon -->
<div
style="display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; gap: 1rem; background: #fffdf7; color: #23160e;">
<svg class="loading-progress" width="48" height="48" viewBox="0 0 80 80">
<circle cx="40" cy="40" r="32" fill="none" stroke="#ede2cc" stroke-width="6" />
<circle cx="40" cy="40" r="32" fill="none" stroke="#8B5E3C" stroke-width="6" stroke-dasharray="200"
stroke-dashoffset="60" style="animation: spin 1s linear infinite; transform-origin: center;">
</circle>
</svg>
<p style="color: #6d4830; font-family: 'Poppins', sans-serif; font-size: 1rem; font-weight: 500;">Loading
Eggymon Kitchen...</p>
</div>
<style>
@media (prefers-color-scheme: dark) {
#app>div {
background: #1a1008 !important;
color: #fef9ed !important;
}
.loading-progress circle:first-child {
stroke: #3a2518 !important;
}
.loading-progress circle:last-child {
stroke: #F5A623 !important;
}
#app p {
color: #d4a574 !important;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</div>
<div id="blazor-error-ui"
style="display: none; position: fixed; bottom: 0; left: 0; right: 0; padding: 1rem; background: #ef4444; color: #fff; text-align: center;">
An unhandled error has occurred. / Đã xảy ra lỗi.
<a href="." class="reload" style="color: #fff; text-decoration: underline; margin-left: 1rem;">Reload / Tải
lại</a>
<span class="dismiss" style="cursor: pointer; margin-left: 1rem;"></span>
</div>
<!-- EN: Blazor WebAssembly -->
<script src="/_framework/blazor.webassembly.js"></script>
<!-- EN: Theme Helper -->
<!-- VI: Helper chuyển đổi theme -->
<script>
window.setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
}
</script>
<script src="/_content/MudBlazor/MudBlazor.min.js"></script>
</body>
</html>

View File

@@ -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"
}

View File

@@ -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ụ"
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EggymonLandingPage.Shared\EggymonLandingPage.Shared.csproj" />
<ProjectReference Include="..\EggymonLandingPage.Client\EggymonLandingPage.Client.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,117 @@
/// <summary>
/// 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.
/// </summary>
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("<base href=\"/\" />", $"<base href=\"/{culture}/\" />")
.Replace("<base href=\"/\"/>", $"<base href=\"/{culture}/\" />");
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();

View File

@@ -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"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,6 @@
{
"ReverseProxy": {
"Routes": {},
"Clusters": {}
}
}

View File

@@ -0,0 +1,12 @@
namespace EggymonLandingPage.Shared;
/// <summary>
/// EN: Standard API response wrapper.
/// VI: Wrapper response API chuẩn.
/// </summary>
public class ApiResponse<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
public string? Error { get; set; }
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>