feat: add en-US localization file and generated build artifacts for client and server projects
This commit is contained in:
59
apps/web-client-eggymon-landipage-net/Dockerfile
Normal file
59
apps/web-client-eggymon-landipage-net/Dockerfile
Normal 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"]
|
||||
@@ -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>
|
||||
63
apps/web-client-eggymon-landipage-net/README.md
Normal file
63
apps/web-client-eggymon-landipage-net/README.md
Normal 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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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ụ"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ReverseProxy": {
|
||||
"Routes": {},
|
||||
"Clusters": {}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user