refactor(web-client-tpos): centralize theme, fix layout bugs, cleanup dead code

This commit is contained in:
Ho Ngoc Hai
2026-03-01 05:58:58 +07:00
parent cb6337cb7c
commit 33e0bab4f7
7 changed files with 131 additions and 209 deletions

View File

@@ -0,0 +1,57 @@
// EN: Centralized MudBlazor theme definitions for all layouts.
// VI: Định nghĩa MudBlazor theme tập trung cho tất cả layouts.
using MudBlazor;
namespace WebClientTpos.Client;
/// <summary>
/// EN: Centralized theme configuration — used by all layouts (Admin, Auth, POS, Marketing).
/// VI: Cấu hình theme tập trung — sử dụng bởi tất cả layouts (Admin, Auth, POS, Marketing).
/// </summary>
public static class AppTheme
{
/// <summary>
/// EN: Default dark theme — aPOS brand orange (#FF5C00) on dark background.
/// VI: Theme tối mặc định — thương hiệu aPOS cam (#FF5C00) trên nền tối.
/// Used by: AdminLayout, AuthLayout, PosLayout
/// </summary>
public static MudTheme DefaultDark { get; } = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "#1A1A1D",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#1A1A1D",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
/// <summary>
/// EN: Marketing dark theme — yellow accent (#FACC15) for tMarketing module.
/// VI: Theme tối Marketing — điểm nhấn vàng (#FACC15) cho module tMarketing.
/// Used by: MarketingLayout
/// </summary>
public static MudTheme MarketingDark { get; } = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FACC15",
PrimaryContrastText = "#18181B",
AppbarBackground = "#0F0F10",
AppbarText = "#FAFAFA",
Background = "#18181B",
Surface = "#0F0F10",
TextPrimary = "#FAFAFA",
TextSecondary = "#71717A",
ActionDefault = "#FAFAFA",
LinesDefault = "#27272A"
}
};
}

View File

@@ -14,7 +14,7 @@
@inject Microsoft.Extensions.Localization.IStringLocalizer<AdminLayout> L
@using WebClientTpos.Client.Services
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.DefaultDark" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
@@ -135,6 +135,13 @@
@* ═══ MAIN AREA ═══ *@
<main class="admin-main">
@* Mobile-only hamburger toggle *@
<div class="admin-mobile-bar">
<button class="admin-mobile-toggle" @onclick="ToggleSidebar">
<i data-lucide="menu"></i>
</button>
<span class="admin-mobile-bar__title">GoodGo Admin</span>
</div>
<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
<CascadingValue Value="this">
@@ -263,22 +270,6 @@
NavigationManager.NavigateTo("/auth/login", forceLoad: true);
}
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "#1A1A1D",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#1A1A1D",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
public void Dispose()
{

View File

@@ -1,7 +1,8 @@
@inherits LayoutComponentBase
@inject IJSRuntime JS
@inject Microsoft.Extensions.Localization.IStringLocalizer<AuthLayout> L
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.DefaultDark" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
@@ -10,10 +11,10 @@
<div class="tpos-navbar-inner">
<a href="/" class="tpos-logo">aPOS</a>
<div class="tpos-nav-links">
<a href="/#features" class="tpos-nav-link">Tính năng</a>
<a href="/#pricing" class="tpos-nav-link">Bảng giá</a>
<a href="/login" class="tpos-nav-link">Đăng nhập</a>
<a href="/register" class="btn-accent">Dùng thử</a>
<a href="/#features" class="tpos-nav-link">@L["Nav_Features"]</a>
<a href="/#pricing" class="tpos-nav-link">@L["Nav_Pricing"]</a>
<a href="/auth/login" class="tpos-nav-link">@L["Nav_Login"]</a>
<a href="/auth/register" class="btn-accent">@L["Nav_FreeTrial"]</a>
</div>
</div>
</nav>
@@ -29,21 +30,4 @@
{
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }
}
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "rgba(10,10,11,0.85)",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#111113",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
}

View File

@@ -6,8 +6,10 @@
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
@inject WebClientTpos.Client.Services.AuthService AuthSvc
@using WebClientTpos.Client.Services
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.MarketingDark" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
@@ -125,22 +127,12 @@
private void ToggleSidebar() => _sidebarOpen = !_sidebarOpen;
private void CloseSidebar() => _sidebarOpen = false;
private void ToggleSearch() => _searchOpen = !_searchOpen;
private void Logout() => NavigationManager.NavigateTo("/login");
private MudTheme _theme = new()
// EN: Properly clear auth state on logout instead of just navigating
// VI: Xóa auth state đúng cách khi đăng xuất thay vì chỉ điều hướng
private async Task Logout()
{
PaletteDark = new PaletteDark()
{
Primary = "#FACC15",
PrimaryContrastText = "#18181B",
AppbarBackground = "#0F0F10",
AppbarText = "#FAFAFA",
Background = "#18181B",
Surface = "#0F0F10",
TextPrimary = "#FAFAFA",
TextSecondary = "#71717A",
ActionDefault = "#FAFAFA",
LinesDefault = "#27272A"
}
};
await AuthSvc.LogoutAsync();
NavigationManager.NavigateTo("/auth/login", forceLoad: true);
}
}

View File

@@ -8,7 +8,7 @@
@inject NavigationManager NavigationManager
@inject WebClientTpos.Client.Services.PosDataService DataService
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.DefaultDark" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
@@ -65,21 +65,6 @@
private void GoToAdmin() => NavigationManager.NavigateTo("/admin");
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "#1A1A1D",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#1A1A1D",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
}

View File

@@ -1,136 +0,0 @@
@page "/login"
@using WebClientTpos.Shared.DTOs
@using WebClientTpos.Shared
@inject HttpClient Http
@inject NavigationManager Navigation
@inject IStringLocalizer<Login> L
@*
EN: Login page with email/password authentication.
VI: Trang đăng nhập với xác thực email/mật khẩu.
*@
<PageTitle>@L["Auth_Login_Title"]</PageTitle>
<div class="auth-container">
<section class="auth-card">
<h1 class="auth-title">@L["Auth_Login_Title"]</h1>
<p class="auth-subtitle">@L["Auth_Login_Subtitle"]</p>
<EditForm Model="@loginModel" OnValidSubmit="HandleLogin" FormName="LoginForm">
<DataAnnotationsValidator />
<div class="form-group">
<label for="login-email">@L["Auth_Login_Email"] *</label>
<InputText id="login-email"
@bind-Value="loginModel.Email"
class="form-input"
placeholder="email@example.com"
autocomplete="email" />
<ValidationMessage For="() => loginModel.Email" class="validation-message" />
</div>
<div class="form-group">
<label for="login-password">@L["Auth_Login_Password"] *</label>
<InputText id="login-password"
@bind-Value="loginModel.Password"
type="password"
class="form-input"
placeholder="••••••••"
autocomplete="current-password" />
<ValidationMessage For="() => loginModel.Password" class="validation-message" />
</div>
<div class="form-actions-row">
<div class="checkbox-group">
<InputCheckbox id="remember-me"
@bind-Value="loginModel.RememberMe"
class="form-checkbox" />
<label for="remember-me" class="checkbox-label">@L["Auth_Login_RememberMe"]</label>
</div>
<a href="/forgot-password" class="link-secondary">@L["Auth_Login_ForgotPassword"]</a>
</div>
<button type="submit" class="btn-primary btn-full" disabled="@isSubmitting">
@if (isSubmitting)
{
<span class="spinner-small"></span>
<span>@L["Common_Loading"]</span>
}
else
{
@L["Auth_Login_Submit"]
}
</button>
</EditForm>
@if (!string.IsNullOrEmpty(message))
{
<div class="alert @(success ? "alert-success" : "alert-error")">
@message
</div>
}
<div class="auth-footer">
<span>@L["Auth_Login_NoAccount"]</span>
<a href="/register" class="link-primary">@L["Auth_Login_RegisterLink"]</a>
</div>
</section>
</div>
@code {
private LoginDto loginModel = new();
private bool isSubmitting = false;
private string message = "";
private bool success = false;
/// <summary>
/// EN: Handle login form submission.
/// VI: Xử lý submit form đăng nhập.
/// </summary>
private async Task HandleLogin()
{
isSubmitting = true;
message = "";
try
{
var response = await Http.PostAsJsonAsync("api/auth/login", loginModel);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
if (result?.Success == true && result.Data != null)
{
success = true;
message = string.Format(L["Auth_Login_Success"], result.Data.DisplayName);
// EN: Redirect to home after 1 second
// VI: Chuyển hướng về trang chủ sau 1 giây
await Task.Delay(1000);
Navigation.NavigateTo("/");
}
else
{
success = false;
message = L["Auth_Login_Error"];
}
}
else
{
success = false;
message = L["Auth_Login_Error"];
}
}
catch (Exception ex)
{
success = false;
message = $"{L["Common_Error"]}: {ex.Message}";
}
finally
{
isSubmitting = false;
}
}
}

View File

@@ -1518,4 +1518,53 @@ select.admin-form-input {
.admin-status-badge--offline .admin-status-badge__dot {
background-color: var(--admin-danger);
}
/* ═════════════════════════════════════════════════════════════════════════
17. MOBILE BAR (hamburger toggle header — visible ≤ 1024px only)
═════════════════════════════════════════════════════════════════════════ */
/* EN: Hidden on desktop / VI: Ẩn trên desktop */
.admin-mobile-bar {
display: none;
align-items: center;
gap: 12px;
padding: 12px 20px;
background-color: var(--admin-bg-elevated);
border-bottom: 1px solid var(--admin-border-subtle);
}
.admin-mobile-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--admin-radius-md);
background: transparent;
border: 1px solid var(--admin-border-default);
color: var(--admin-text-primary);
cursor: pointer;
transition: background 0.2s ease;
}
.admin-mobile-toggle:hover {
background-color: var(--admin-bg-interactive);
}
.admin-mobile-toggle i {
width: 20px;
height: 20px;
}
.admin-mobile-bar__title {
font-size: 16px;
font-weight: 700;
color: var(--admin-text-primary);
}
@media (max-width: 1024px) {
.admin-mobile-bar {
display: flex;
}
}