From db53e1abaea6932c9382cdbc1b0ed6c7aff5cb3e Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 12 Feb 2026 09:17:47 +0700 Subject: [PATCH] feat: Implement comprehensive authentication UI including login, registration, password reset, OTP verification, and supporting components. --- .../Components/Auth/AuthButton.razor | 69 ++ .../Components/Auth/AuthCard.razor | 157 +++ .../Components/Auth/AuthInput.razor | 75 ++ .../Components/Auth/BrandPanel.razor | 95 ++ .../Components/Auth/OtpInput.razor | 89 ++ .../Components/Auth/SocialLogin.razor | 39 + .../Pages/Auth/EmailSent.razor | 45 + .../Pages/Auth/ForgotPasswordNew.razor | 74 ++ .../Pages/Auth/LoginAdmin.razor | 61 + .../Pages/Auth/LoginBranch.razor | 56 + .../Pages/Auth/LoginCustomer.razor | 68 ++ .../Pages/Auth/LoginStaff.razor | 68 ++ .../Pages/Auth/OtpVerify.razor | 43 + .../Pages/Auth/PasswordResetNew.razor | 84 ++ .../Pages/Auth/RegisterCustomer.razor | 85 ++ .../Pages/Auth/TwoFactorAuth.razor | 39 + .../WebClientTpos.Client/wwwroot/css/auth.css | 1058 +++++++++++++++++ .../WebClientTpos.Client/wwwroot/index.html | 1 + 18 files changed, 2206 insertions(+) create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthButton.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthCard.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthInput.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/BrandPanel.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/OtpInput.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/SocialLogin.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginCustomer.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/auth.css diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthButton.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthButton.razor new file mode 100644 index 00000000..a3818096 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthButton.razor @@ -0,0 +1,69 @@ +@* + EN: Auth CTA button with color variants (orange, blue, green, outline, ghost). + VI: Nút CTA xác thực với các biến thể màu (cam, xanh dương, xanh lá, outline, ghost). +*@ + + + +@code { + /// + /// EN: Button color variant. Options: orange, blue, green, outline, ghost. + /// VI: Biến thể màu. Tùy chọn: orange, blue, green, outline, ghost. + /// + [Parameter] public string Variant { get; set; } = "orange"; + + /// + /// EN: HTML button type attribute. + /// VI: Attribute type của button. + /// + [Parameter] public string ButtonType { get; set; } = "button"; + + /// + /// EN: Whether the button is disabled. + /// VI: Button có bị disable không. + /// + [Parameter] public bool Disabled { get; set; } + + /// + /// EN: Whether to show a loading spinner. + /// VI: Có hiển thị spinner loading không. + /// + [Parameter] public bool Loading { get; set; } + + /// + /// EN: Optional Lucide icon name. + /// VI: Tên icon Lucide tùy chọn. + /// + [Parameter] public string? IconName { get; set; } + + /// + /// EN: Additional CSS classes. + /// VI: CSS class bổ sung. + /// + [Parameter] public string? CssClass { get; set; } + + /// + /// EN: Click event callback. + /// VI: Callback sự kiện click. + /// + [Parameter] public EventCallback OnClick { get; set; } + + /// + /// EN: Button content. + /// VI: Nội dung button. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthCard.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthCard.razor new file mode 100644 index 00000000..e6203d9a --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthCard.razor @@ -0,0 +1,157 @@ +@* + EN: Reusable auth card wrapper — centered form container with icon, title, subtitle. + VI: Card xác thực tái sử dụng — container form giữa trang với icon, tiêu đề, mô tả. +*@ + +
+ @if (BackLink != null) + { + + @BackLinkText + + } + + @if (ShowHeader) + { +
+ @if (Icon != null) + { +
+ +
+ } + @if (RoleBadge != null) + { + @RoleBadge + } + @if (Title != null) + { +

@Title

+ } + @if (Subtitle != null) + { +

@Subtitle

+ } + @if (SecurityHint != null) + { +
+ + @SecurityHint +
+ } +
+ } + + @ChildContent + + @if (FooterContent != null) + { + + } +
+ +@code { + /// + /// EN: Card title text. + /// VI: Tiêu đề card. + /// + [Parameter] public string? Title { get; set; } + + /// + /// EN: Subtitle/description text shown below the title. + /// VI: Mô tả hiển thị dưới tiêu đề. + /// + [Parameter] public string? Subtitle { get; set; } + + /// + /// EN: Lucide icon name shown above the title. + /// VI: Tên icon Lucide hiển thị trên tiêu đề. + /// + [Parameter] public string? Icon { get; set; } + + /// + /// EN: CSS class for the icon (e.g., auth-icon--blue, auth-icon--orange). + /// VI: CSS class cho icon (VD: auth-icon--blue, auth-icon--orange). + /// + [Parameter] public string IconClass { get; set; } = "auth-icon--blue"; + + /// + /// EN: Role badge text (e.g., "QUẢN TRỊ", "NHÂN VIÊN"). + /// VI: Text badge vai trò (VD: "QUẢN TRỊ", "NHÂN VIÊN"). + /// + [Parameter] public string? RoleBadge { get; set; } + + /// + /// EN: CSS class for the role badge. + /// VI: CSS class cho role badge. + /// + [Parameter] public string RoleBadgeClass { get; set; } = "auth-role-badge--blue"; + + /// + /// EN: Security hint message (e.g., "Khu vực bảo mật cao"). + /// VI: Thông báo bảo mật. + /// + [Parameter] public string? SecurityHint { get; set; } + + /// + /// EN: Icon for the security hint. + /// VI: Icon cho thông báo bảo mật. + /// + [Parameter] public string SecurityHintIcon { get; set; } = "shield-alert"; + + /// + /// EN: CSS class for the security hint. + /// VI: CSS class cho thông báo bảo mật. + /// + [Parameter] public string SecurityHintClass { get; set; } = "auth-security-hint--warning"; + + /// + /// EN: Whether to show the header section. + /// VI: Có hiển thị phần header không. + /// + [Parameter] public bool ShowHeader { get; set; } = true; + + /// + /// EN: Use compact gap (28px vs 32px). + /// VI: Dùng gap nhỏ hơn (28px thay vì 32px). + /// + [Parameter] public bool Compact { get; set; } + + /// + /// EN: Transparent background (used inside split panels). + /// VI: Nền trong suốt (dùng trong split panel). + /// + [Parameter] public bool Transparent { get; set; } + + /// + /// EN: Back link URL. + /// VI: URL link quay lại. + /// + [Parameter] public string? BackLink { get; set; } + + /// + /// EN: Back link text. + /// VI: Text link quay lại. + /// + [Parameter] public string BackLinkText { get; set; } = "Quay lại đăng nhập"; + + /// + /// EN: Additional CSS classes. + /// VI: CSS class bổ sung. + /// + [Parameter] public string? CssClass { get; set; } + + /// + /// EN: Main content slot. + /// VI: Slot nội dung chính. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// EN: Footer content slot. + /// VI: Slot nội dung footer. + /// + [Parameter] public RenderFragment? FooterContent { get; set; } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthInput.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthInput.razor new file mode 100644 index 00000000..0f1c341c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthInput.razor @@ -0,0 +1,75 @@ +@* + EN: Dark-themed auth input with label, prefix icon, and action button (e.g., password toggle). + VI: Input xác thực dark theme với label, icon prefix, và action button (VD: toggle mật khẩu). +*@ + +
+ @if (Label != null || ForgotPasswordLink != null) + { +
+ @if (Label != null) + { + + } + @if (ForgotPasswordLink != null) + { + @ForgotPasswordText + } +
+ } +
+ @if (PrefixIcon != null) + { + + + + } + + @if (InputType == "password") + { + + } +
+
+ +@code { + private bool _showPassword = false; + private string _currentType => InputType == "password" && _showPassword ? "text" : InputType; + + [Parameter] public string? Label { get; set; } + [Parameter] public string InputType { get; set; } = "text"; + [Parameter] public string? Placeholder { get; set; } + [Parameter] public string? Value { get; set; } + [Parameter] public string? PrefixIcon { get; set; } + [Parameter] public string? InputId { get; set; } + [Parameter] public string? AutoComplete { get; set; } + [Parameter] public string? ForgotPasswordLink { get; set; } + [Parameter] public string ForgotPasswordText { get; set; } = "Quên mật khẩu?"; + [Parameter] public EventCallback ValueChanged { get; set; } + + private async Task OnInput(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(e); + } + + private async Task OnChange(ChangeEventArgs e) + { + Value = e.Value?.ToString(); + await ValueChanged.InvokeAsync(e); + } + + private void TogglePassword() + { + _showPassword = !_showPassword; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/BrandPanel.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/BrandPanel.razor new file mode 100644 index 00000000..15b94355 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/BrandPanel.razor @@ -0,0 +1,95 @@ +@* + EN: Brand panel for split auth layouts — shows logo, title, description, and trust stats. + VI: Panel thương hiệu cho layout chia đôi — hiển thị logo, tiêu đề, mô tả, thống kê. +*@ + +
+ +

@Title

+

@Description

+ + @if (ShowStats) + { +
+ @foreach (var stat in Stats) + { +
+ @stat.Value + @stat.Label +
+ } +
+ } + + @if (Features != null && Features.Count > 0) + { +
+ @foreach (var feature in Features) + { +
+ + @feature +
+ } +
+ } + + @ChildContent +
+ +@code { + /// + /// EN: Panel variant class (auth-brand-panel--orange or auth-brand-panel--dark). + /// VI: Class biến thể panel (auth-brand-panel--orange hoặc auth-brand-panel--dark). + /// + [Parameter] public string PanelClass { get; set; } = "auth-brand-panel--orange"; + + /// + /// EN: Logo character/text displayed in the brand panel. + /// VI: Ký tự/text logo hiển thị trong brand panel. + /// + [Parameter] public string LogoText { get; set; } = "a"; + + /// + /// EN: Brand panel title. + /// VI: Tiêu đề brand panel. + /// + [Parameter] public string Title { get; set; } = "aPOS Branch"; + + /// + /// EN: Description text. + /// VI: Text mô tả. + /// + [Parameter] public string Description { get; set; } = ""; + + /// + /// EN: Whether to show trust stats. + /// VI: Có hiển thị thống kê tin cậy không. + /// + [Parameter] public bool ShowStats { get; set; } = true; + + /// + /// EN: Trust statistics data. + /// VI: Dữ liệu thống kê tin cậy. + /// + [Parameter] public List Stats { get; set; } = new() + { + new("1,200+", "Cửa hàng"), + new("50K+", "Giao dịch/ngày"), + new("99.9%", "Uptime") + }; + + /// + /// EN: Feature list items. + /// VI: Danh sách tính năng. + /// + [Parameter] public List? Features { get; set; } + + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// EN: Brand stat record. + /// VI: Record thống kê thương hiệu. + /// + public record BrandStat(string Value, string Label); +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/OtpInput.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/OtpInput.razor new file mode 100644 index 00000000..5dfa7fa2 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/OtpInput.razor @@ -0,0 +1,89 @@ +@* + EN: 6-digit OTP input with auto-focus, auto-advance, and backspace support. + VI: Input OTP 6 chữ số với auto-focus, tự chuyển ô, và hỗ trợ backspace. +*@ + +@inject IJSRuntime JS + +
+ @for (int i = 0; i < DigitCount; i++) + { + var index = i; + + } +
+ +@code { + private string[] _digits = new string[6]; + + /// + /// EN: Number of OTP digits. + /// VI: Số chữ số OTP. + /// + [Parameter] public int DigitCount { get; set; } = 6; + + /// + /// EN: Use blue theme (for 2FA authenticator). + /// VI: Dùng theme xanh dương (cho 2FA authenticator). + /// + [Parameter] public bool UseBlueTheme { get; set; } + + /// + /// EN: Callback when all digits are entered. + /// VI: Callback khi tất cả chữ số được nhập. + /// + [Parameter] public EventCallback OnComplete { get; set; } + + protected override void OnInitialized() + { + _digits = new string[DigitCount]; + } + + private async Task HandleInput(ChangeEventArgs e, int index) + { + var value = e.Value?.ToString() ?? ""; + + // EN: Only allow numeric input + // VI: Chỉ cho phép nhập số + if (!string.IsNullOrEmpty(value) && !char.IsDigit(value[0])) + { + _digits[index] = ""; + return; + } + + _digits[index] = value; + + if (!string.IsNullOrEmpty(value) && index < DigitCount - 1) + { + // EN: Auto-advance to next input + // VI: Tự động chuyển sang ô tiếp theo + await JS.InvokeVoidAsync("eval", $"document.getElementById('otp-{index + 1}')?.focus()"); + } + + // EN: Check if all digits are filled + // VI: Kiểm tra tất cả ô đã nhập xong chưa + if (_digits.All(d => !string.IsNullOrEmpty(d))) + { + var code = string.Join("", _digits); + await OnComplete.InvokeAsync(code); + } + } + + private async Task HandleKeyDown(KeyboardEventArgs e, int index) + { + if (e.Key == "Backspace" && string.IsNullOrEmpty(_digits[index]) && index > 0) + { + // EN: Move to previous input on backspace + // VI: Chuyển về ô trước khi nhấn backspace + _digits[index - 1] = ""; + await JS.InvokeVoidAsync("eval", $"document.getElementById('otp-{index - 1}')?.focus()"); + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/SocialLogin.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/SocialLogin.razor new file mode 100644 index 00000000..57c6cb3c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/SocialLogin.razor @@ -0,0 +1,39 @@ +@* + EN: Social login buttons — Google, Apple, Zalo with divider. + VI: Nút đăng nhập bên thứ ba — Google, Apple, Zalo với divider. +*@ + +
@DividerText
+ +
+ + + @if (ShowZalo) + { + + } +
+ +@code { + /// + /// EN: Divider text between form and social buttons. + /// VI: Text divider giữa form và social buttons. + /// + [Parameter] public string DividerText { get; set; } = "Hoặc đăng nhập bằng"; + + /// + /// EN: Whether to show Zalo button (Vietnam market). + /// VI: Có hiển thị nút Zalo không (thị trường VN). + /// + [Parameter] public bool ShowZalo { get; set; } = true; + + [Parameter] public EventCallback OnGoogleClick { get; set; } + [Parameter] public EventCallback OnAppleClick { get; set; } + [Parameter] public EventCallback OnZaloClick { get; set; } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor new file mode 100644 index 00000000..2bb12243 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor @@ -0,0 +1,45 @@ +@page "/auth/email-sent" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Email Sent Confirmation page — shows email delivery success with action buttons. + VI: Trang xác nhận đã gửi email — hiển thị gửi email thành công với nút hành động. + Design: pencil-design/src/pages/tPOS/auth/workflow/email-sent.pen +*@ + +Kiểm tra email của bạn + +
+ + + u****a@gmail.com + + + Mở ứng dụng Email + + + + Mở ứng dụng Email + + + + + Không nhận được email? + + + Kiểm tra spam hoặc gửi lại + + + Quay lại đăng nhập + + + +
+ +@code { +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor new file mode 100644 index 00000000..0625e634 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor @@ -0,0 +1,74 @@ +@page "/auth/forgot-password-new" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Forgot password page — centered card, email/phone input → sends reset link. + VI: Trang quên mật khẩu — card giữa, input email/SĐT → gửi link đặt lại. + Design: pencil-design/src/pages/tPOS/auth/forgot-password/desktop.pen +*@ + +Quên mật khẩu + +
+ @if (!_resetSent) + { + + +
+ +
+ + + Gửi yêu cầu + +
+
+ } + else + { + + + user@example.com + + + Mở ứng dụng email + + + + Gửi lại email + + + + + Không nhận được email? + + + Kiểm tra spam hoặc thử lại với số khác + + + + } +
+ +@code { + private bool _resetSent = false; + + private void HandleSubmit() + { + _resetSent = true; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor new file mode 100644 index 00000000..e8759e6b --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor @@ -0,0 +1,61 @@ +@page "/auth/login/admin" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Admin login page — centered card layout, blue CTA, security badge. + VI: Trang đăng nhập Admin — layout card giữa, CTA xanh dương, badge bảo mật. + Design: pencil-design/src/pages/tPOS/auth/login/admin-desktop.pen +*@ + +Đăng nhập Admin + +
+ + +
+ + + +
+ + + Đăng nhập bảo mật + +
+ +
+ Đăng nhập với vai trò khác + +
+
+
+
+ +@code { +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor new file mode 100644 index 00000000..edad05aa --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor @@ -0,0 +1,56 @@ +@page "/auth/login/branch" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Branch login page — split panel layout, orange CTA, trust stats. + VI: Trang đăng nhập Chi nhánh — layout chia đôi, CTA cam, thống kê tin cậy. + Design: pencil-design/src/pages/tPOS/auth/login/branch-desktop.pen +*@ + +Đăng nhập Chi nhánh + +
+ + +
+ + +
+ + + +
+ + + Đăng nhập + +
+ + + Chưa có tài khoản? Liên hệ + + +
+
+
+ +@code { +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginCustomer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginCustomer.razor new file mode 100644 index 00000000..fe9a87d4 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginCustomer.razor @@ -0,0 +1,68 @@ +@page "/auth/login/customer" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Customer login page — split panel layout, phone OTP + social login. + VI: Trang đăng nhập Khách hàng — layout chia đôi, OTP điện thoại + social login. + Design: pencil-design/src/pages/tPOS/auth/login/customer-desktop.pen +*@ + +Đăng nhập Khách hàng + +
+ + +
+ + +
+
+ +
+ 🇻🇳 +84 + +
+
+
+ + + Nhận mã OTP + + + +
+ + + Chưa có tài khoản? Đăng ký ngay + +

+ Bằng việc tiếp tục, bạn đồng ý với + Điều khoản sử dụng và + Chính sách bảo mật +

+
+
+
+
+ +@code { + private List _features = new() + { + "Tích điểm tự động mỗi lần mua hàng", + "Đổi điểm lấy quà tặng hấp dẫn", + "Nhận thông báo ưu đãi độc quyền", + "Quản lý đơn hàng và lịch sử giao dịch" + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor new file mode 100644 index 00000000..976c385b --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor @@ -0,0 +1,68 @@ +@page "/auth/login/staff" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Staff login page — centered card layout, green CTA, role hints. + VI: Trang đăng nhập Nhân viên — layout card giữa, CTA xanh lá, gợi ý vai trò. + Design: pencil-design/src/pages/tPOS/auth/login/staff-desktop.pen +*@ + +Đăng nhập Nhân viên + +
+ + +
+ + + +
+ + + Đăng nhập ca làm việc + +
+ +
+ Vai trò hỗ trợ +
+ + Thu ngân + + + Barista + + + Phục vụ + + + Quản lý + +
+
+ + Quên mật khẩu? Liên hệ quản lý + +
+
+
+ +@code { +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor new file mode 100644 index 00000000..c5db01ab --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor @@ -0,0 +1,43 @@ +@page "/auth/otp-verify" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: OTP Verification page — 6-digit code input with countdown timer. + VI: Trang xác thực OTP — input 6 chữ số với đếm ngược. + Design: pencil-design/src/pages/tPOS/auth/workflow/otp-verify.pen +*@ + +Xác thực OTP + +
+ + + + + + Xác nhận + + + +
+ + Mã hết hạn sau: 04:30 +
+ + Không nhận được mã? Gửi lại + +
+
+
+ +@code { + private async Task HandleOtpComplete(string code) + { + Console.WriteLine($"OTP entered: {code}"); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor new file mode 100644 index 00000000..f49404ef --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor @@ -0,0 +1,84 @@ +@page "/auth/password-reset" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Password Reset page — new password form with strength indicator. + VI: Trang đặt lại mật khẩu — form mật khẩu mới với chỉ báo độ mạnh. + Design: pencil-design/src/pages/tPOS/auth/workflow/password-reset.pen +*@ + +Đặt lại mật khẩu + +
+ @if (!_resetSuccess) + { + + +
+ + + + +
+ Yêu cầu mật khẩu +
+ Ít nhất 8 ký tự +
+
+ Chứa chữ hoa và chữ thường +
+
+ Chứa ít nhất 1 số hoặc 1 ký tự đặc biệt +
+
+
+ + + Đặt lại mật khẩu + +
+
+ } + else + { + + + + Đăng nhập ngay + + + + } +
+ +@code { + private bool _resetSuccess = false; + + private void HandleReset() + { + _resetSuccess = true; + } + + private void GoToLogin() + { + Navigation.NavigateTo("/auth/login/branch"); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor new file mode 100644 index 00000000..ec5b8365 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor @@ -0,0 +1,85 @@ +@page "/auth/register/customer" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Customer registration page — split panel layout, phone + social registration. + VI: Trang đăng ký Khách hàng — layout chia đôi, đăng ký bằng SĐT + mạng xã hội. + Design: pencil-design/src/pages/tPOS/auth/register/customer-desktop.pen +*@ + +Đăng ký tài khoản + +
+ + +
+ + +
+ + +
+ +
+ 🇻🇳 +84 + +
+
+ + + +
+ + +
+
+ + + Đăng ký + + + +
+ + + Đã có tài khoản? Đăng nhập + + +
+
+
+ +@code { + private List _features = new() + { + "Đăng ký nhanh trong 30 giây", + "Nhận ngay 100 điểm thưởng", + "Ưu đãi sinh nhật đặc biệt", + "Lịch sử mua hàng & đơn hàng" + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor new file mode 100644 index 00000000..6dd757ce --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor @@ -0,0 +1,39 @@ +@page "/auth/two-factor" +@using WebClientTpos.Client.Components.Auth +@inject NavigationManager Navigation + +@* + EN: Two-Factor Authentication page — 6-digit authenticator code input, blue theme. + VI: Trang xác thực 2 lớp — input 6 chữ số từ authenticator app, theme xanh dương. + Design: pencil-design/src/pages/tPOS/auth/workflow/two-factor-auth.pen +*@ + +Xác thực 2 lớp + +
+ + + + + + Xác nhận + + + + + Không thể sử dụng Authenticator? Dùng backup code + + + +
+ +@code { + private async Task Handle2FAComplete(string code) + { + Console.WriteLine($"2FA code entered: {code}"); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/auth.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/auth.css new file mode 100644 index 00000000..2d8c8ca8 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/auth.css @@ -0,0 +1,1058 @@ +/* ═══════════════════════════════════════════════════════════════════════════════ + Auth Pages — CSS Foundation + EN: Styles for all auth pages (Login, Register, Forgot Password, Workflows) + VI: Styles cho tất cả trang xác thực (Đăng nhập, Đăng ký, Quên MK, Workflows) + Based on: pencil-design/src/pages/tPOS/auth/ tokens + ═══════════════════════════════════════════════════════════════════════════════ */ + +/* ═════════════════════════════════════════════════════════════════════════ + 1. AUTH-SPECIFIC VARIABLES + ═════════════════════════════════════════════════════════════════════════ */ +:root { + /* Auth card */ + --auth-card-bg: #111113; + --auth-card-border: #1F1F23; + --auth-card-radius: 24px; + --auth-card-width: 440px; + --auth-card-padding: 48px; + + /* Auth inputs */ + --auth-input-bg: #0A0A0B; + --auth-input-border: #2A2A2E; + --auth-input-border-focus: #FF5C00; + --auth-input-radius: 12px; + --auth-input-height: 48px; + + /* Auth CTA colors */ + --auth-cta-blue: #3B82F6; + --auth-cta-blue-hover: #2563EB; + --auth-cta-orange: #FF5C00; + --auth-cta-orange-hover: #E05200; + --auth-cta-green: #22C55E; + --auth-cta-green-hover: #16A34A; + + /* Brand panel */ + --auth-brand-orange: linear-gradient(180deg, #FF5C00 0%, #FF8A4C 50%, #FFB347 100%); + --auth-brand-dark: linear-gradient(135deg, #0A0A0B 0%, #1A1A1D 50%, #0A0A0B 100%); + + /* OTP input */ + --auth-otp-size: 52px; + --auth-otp-bg: #1A1A1D; + --auth-otp-border: #2A2A2E; + --auth-otp-border-focus: #FF5C00; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 2. AUTH PAGE LAYOUT — Full page containers + ═════════════════════════════════════════════════════════════════════════ */ + +/* EN: Centered card layout (admin, staff, forgot-password, workflows) */ +/* VI: Layout card giữa trang (admin, staff, quên MK, workflows) */ +.auth-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background-color: var(--bg-page); + padding: 24px; +} + +/* EN: Split panel layout (branch, customer, register) */ +/* VI: Layout chia đôi (chi nhánh, khách hàng, đăng ký) */ +.auth-page--split { + display: flex; + min-height: 100vh; + background-color: var(--bg-page); + padding: 0; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 3. AUTH CARD — Central form container + ═════════════════════════════════════════════════════════════════════════ */ +.auth-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; + width: 100%; + max-width: var(--auth-card-width); + padding: var(--auth-card-padding); + background-color: var(--auth-card-bg); + border: 1px solid var(--auth-card-border); + border-radius: var(--auth-card-radius); +} + +.auth-card--compact { + gap: 28px; +} + +.auth-card--transparent { + background-color: transparent; + border: none; + padding: 0; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 4. AUTH HEADER — Icon + Title + Subtitle + ═════════════════════════════════════════════════════════════════════════ */ +.auth-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + width: 100%; +} + +.auth-icon { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 16px; + margin-bottom: 4px; +} + +.auth-icon--blue { + background-color: rgba(59, 130, 246, 0.15); + color: var(--auth-cta-blue); +} + +.auth-icon--orange { + background-color: rgba(255, 92, 0, 0.15); + color: var(--auth-cta-orange); +} + +.auth-icon--green { + background-color: rgba(34, 197, 94, 0.15); + color: var(--auth-cta-green); +} + +.auth-icon--success { + background-color: rgba(34, 197, 94, 0.15); + color: var(--auth-cta-green); + border-radius: 50%; +} + +.auth-icon svg, +.auth-icon .lucide { + width: 28px; + height: 28px; + stroke-width: 2; + background: none !important; +} + +.auth-role-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 14px; + border-radius: 100px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.auth-role-badge--blue { + background-color: rgba(59, 130, 246, 0.15); + color: var(--auth-cta-blue); + border: 1px solid rgba(59, 130, 246, 0.25); +} + +.auth-role-badge--green { + background-color: rgba(34, 197, 94, 0.15); + color: var(--auth-cta-green); + border: 1px solid rgba(34, 197, 94, 0.25); +} + +.auth-heading { + font-size: 1.625rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1.2; + margin: 0; +} + +.auth-subheading { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.5; + margin: 0; + max-width: 360px; +} + +.auth-security-hint { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 10px; + font-size: 0.813rem; + width: 100%; +} + +.auth-security-hint--warning { + background-color: rgba(255, 92, 0, 0.08); + border: 1px solid rgba(255, 92, 0, 0.2); + color: var(--auth-cta-orange); +} + +.auth-security-hint--info { + background-color: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.2); + color: var(--auth-cta-blue); +} + +.auth-security-hint svg { + width: 16px; + height: 16px; + flex-shrink: 0; + background: none !important; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 5. AUTH FORM — Inputs, labels, groups + ═════════════════════════════════════════════════════════════════════════ */ +.auth-form { + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +} + +.auth-field { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.auth-label { + font-size: 0.813rem; + font-weight: 500; + color: var(--text-secondary); +} + +.auth-label-row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.auth-label-link { + font-size: 0.813rem; + color: var(--auth-cta-orange); + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.auth-label-link:hover { + color: var(--accent-light); + text-decoration: underline; +} + +.auth-input-wrapper { + position: relative; + display: flex; + align-items: center; + width: 100%; +} + +.auth-input { + display: flex; + align-items: center; + width: 100%; + height: var(--auth-input-height); + padding: 0 16px 0 44px; + background-color: var(--auth-input-bg); + border: 1px solid var(--auth-input-border); + border-radius: var(--auth-input-radius); + color: var(--text-primary); + font-size: 0.875rem; + font-family: var(--font-body); + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.auth-input:focus { + border-color: var(--auth-input-border-focus); + box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.1); +} + +.auth-input::placeholder { + color: var(--text-disabled); +} + +.auth-input--no-icon { + padding-left: 16px; +} + +.auth-input-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--text-tertiary); + pointer-events: none; +} + +.auth-input-icon svg { + width: 18px; + height: 18px; + background: none !important; +} + +.auth-input-action { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-tertiary); + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + transition: color 0.2s; +} + +.auth-input-action:hover { + color: var(--text-primary); +} + +.auth-input-action svg { + width: 18px; + height: 18px; + background: none !important; +} + +/* EN: Phone input with country code */ +/* VI: Input điện thoại với mã quốc gia */ +.auth-phone-group { + display: flex; + gap: 8px; + width: 100%; +} + +.auth-phone-prefix { + display: flex; + align-items: center; + justify-content: center; + height: var(--auth-input-height); + padding: 0 14px; + background-color: var(--auth-input-bg); + border: 1px solid var(--auth-input-border); + border-radius: var(--auth-input-radius); + color: var(--text-secondary); + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; + min-width: 70px; +} + +.auth-phone-input { + flex: 1; + height: var(--auth-input-height); + padding: 0 16px; + background-color: var(--auth-input-bg); + border: 1px solid var(--auth-input-border); + border-radius: var(--auth-input-radius); + color: var(--text-primary); + font-size: 0.875rem; + font-family: var(--font-body); + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.auth-phone-input:focus { + border-color: var(--auth-input-border-focus); + box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.1); +} + +.auth-phone-input::placeholder { + color: var(--text-disabled); +} + +/* EN: Checkbox + Terms group */ +/* VI: Group checkbox + Điều khoản */ +.auth-checkbox-row { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 0.813rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.auth-checkbox-row input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + border: 1px solid var(--auth-input-border); + border-radius: 4px; + background-color: var(--auth-input-bg); + flex-shrink: 0; + cursor: pointer; + margin-top: 2px; + position: relative; +} + +.auth-checkbox-row input[type="checkbox"]:checked { + background-color: var(--auth-cta-orange); + border-color: var(--auth-cta-orange); +} + +.auth-checkbox-row input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 12px; + font-weight: 700; +} + +.auth-checkbox-row a { + color: var(--auth-cta-orange); + text-decoration: none; +} + +.auth-checkbox-row a:hover { + text-decoration: underline; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 6. AUTH BUTTON — CTA variants + ═════════════════════════════════════════════════════════════════════════ */ +.auth-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 48px; + border: none; + border-radius: var(--auth-input-radius); + font-size: 0.938rem; + font-weight: 600; + font-family: var(--font-body); + cursor: pointer; + transition: background-color 0.2s, transform 0.1s, box-shadow 0.2s; +} + +.auth-btn:hover { + transform: translateY(-1px); +} + +.auth-btn:active { + transform: translateY(0); +} + +.auth-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.auth-btn svg { + width: 18px; + height: 18px; + background: none !important; +} + +.auth-btn--orange { + background-color: var(--auth-cta-orange); + color: white; +} + +.auth-btn--orange:hover:not(:disabled) { + background-color: var(--auth-cta-orange-hover); + box-shadow: 0 4px 16px rgba(255, 92, 0, 0.3); +} + +.auth-btn--blue { + background-color: var(--auth-cta-blue); + color: white; +} + +.auth-btn--blue:hover:not(:disabled) { + background-color: var(--auth-cta-blue-hover); + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3); +} + +.auth-btn--green { + background-color: var(--auth-cta-green); + color: white; +} + +.auth-btn--green:hover:not(:disabled) { + background-color: var(--auth-cta-green-hover); + box-shadow: 0 4px 16px rgba(34, 197, 94, 0.3); +} + +.auth-btn--outline { + background-color: transparent; + color: var(--text-primary); + border: 1px solid var(--auth-card-border); +} + +.auth-btn--outline:hover:not(:disabled) { + background-color: var(--bg-elevated); + box-shadow: none; +} + +.auth-btn--ghost { + background-color: transparent; + color: var(--text-secondary); + height: auto; + padding: 8px 16px; +} + +.auth-btn--ghost:hover:not(:disabled) { + color: var(--text-primary); + box-shadow: none; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 7. BRAND PANEL — Left panel for split layouts + ═════════════════════════════════════════════════════════════════════════ */ +.auth-brand-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px; + min-height: 100vh; +} + +.auth-brand-panel--orange { + background: var(--auth-brand-orange); + width: 640px; + flex-shrink: 0; +} + +.auth-brand-panel--dark { + background: var(--auth-brand-dark); + width: 800px; + flex-shrink: 0; +} + +.auth-brand-logo { + display: flex; + align-items: center; + justify-content: center; + width: 72px; + height: 72px; + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.15); + font-size: 2rem; + font-weight: 700; + color: white; + margin-bottom: 24px; +} + +.auth-brand-panel--dark .auth-brand-logo { + background-color: var(--auth-cta-orange); +} + +.auth-brand-title { + font-size: 1.75rem; + font-weight: 700; + color: white; + text-align: center; + margin: 0 0 12px 0; +} + +.auth-brand-desc { + font-size: 0.938rem; + color: rgba(255, 255, 255, 0.8); + text-align: center; + line-height: 1.6; + max-width: 400px; + margin: 0 0 40px 0; +} + +/* EN: Trust stats row */ +/* VI: Hàng thống kê tin cậy */ +.auth-brand-stats { + display: flex; + gap: 40px; + margin-top: 24px; +} + +.auth-brand-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.auth-brand-stat-value { + font-size: 1.5rem; + font-weight: 700; + color: white; +} + +.auth-brand-stat-label { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + text-transform: capitalize; +} + +/* EN: Feature list on brand panel */ +/* VI: Danh sách tính năng trên brand panel */ +.auth-brand-features { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 20px; +} + +.auth-brand-feature { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.85); +} + +.auth-brand-feature svg { + width: 16px; + height: 16px; + color: var(--auth-cta-orange); + background: none !important; +} + +/* EN: Auth panel (right side) */ +/* VI: Panel xác thực (bên phải) */ +.auth-form-panel { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-height: 100vh; + padding: 60px; + background-color: var(--bg-surface); +} + +.auth-form-panel .auth-card { + background: transparent; + border: none; + padding: 0; + max-width: 400px; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 8. SOCIAL LOGIN — Google, Apple, Zalo buttons + ═════════════════════════════════════════════════════════════════════════ */ +.auth-divider { + display: flex; + align-items: center; + gap: 16px; + width: 100%; + font-size: 0.813rem; + color: var(--text-tertiary); +} + +.auth-divider::before, +.auth-divider::after { + content: ''; + flex: 1; + height: 1px; + background-color: var(--border-subtle); +} + +.auth-social-group { + display: flex; + gap: 12px; + justify-content: center; +} + +.auth-social-btn { + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 14px; + border: 1px solid var(--auth-card-border); + background-color: var(--bg-elevated); + cursor: pointer; + transition: border-color 0.2s, background-color 0.2s; +} + +.auth-social-btn:hover { + border-color: var(--border-strong); + background-color: var(--bg-interactive); +} + +.auth-social-btn--google { + color: #FF5C00; + font-weight: 700; + font-size: 1.25rem; +} + +.auth-social-btn--apple { + color: white; + font-size: 1.25rem; +} + +.auth-social-btn--zalo { + background-color: #0068FF; + border-color: #0068FF; + color: white; + font-weight: 700; + font-size: 0.875rem; +} + +.auth-social-btn--zalo:hover { + background-color: #0054cc; + border-color: #0054cc; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 9. OTP INPUT — 6-digit code boxes + ═════════════════════════════════════════════════════════════════════════ */ +.auth-otp-group { + display: flex; + gap: 10px; + justify-content: center; + width: 100%; +} + +.auth-otp-input { + width: var(--auth-otp-size); + height: var(--auth-otp-size); + text-align: center; + font-size: 1.375rem; + font-weight: 600; + color: var(--text-primary); + background-color: var(--auth-otp-bg); + border: 1px solid var(--auth-otp-border); + border-radius: 12px; + outline: none; + font-family: var(--font-body); + transition: border-color 0.2s, box-shadow 0.2s; + caret-color: var(--auth-cta-orange); +} + +.auth-otp-input:focus { + border-color: var(--auth-otp-border-focus); + box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.1); +} + +.auth-otp-input--filled { + border-color: var(--auth-cta-orange); + background-color: rgba(255, 92, 0, 0.05); +} + +.auth-otp-input--blue:focus { + border-color: var(--auth-cta-blue); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.auth-otp-input--blue.auth-otp-input--filled { + border-color: var(--auth-cta-blue); + background-color: rgba(59, 130, 246, 0.05); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 10. PASSWORD STRENGTH — Checklist + ═════════════════════════════════════════════════════════════════════════ */ +.auth-strength { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + padding: 14px 16px; + background-color: var(--bg-page); + border: 1px solid var(--auth-card-border); + border-radius: 12px; +} + +.auth-strength-title { + font-size: 0.813rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.auth-strength-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.813rem; + color: var(--text-tertiary); +} + +.auth-strength-item--pass { + color: var(--auth-cta-green); +} + +.auth-strength-item svg { + width: 14px; + height: 14px; + flex-shrink: 0; + background: none !important; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 11. AUTH FOOTER — Links, navigation, timer + ═════════════════════════════════════════════════════════════════════════ */ +.auth-footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + width: 100%; +} + +.auth-footer-text { + font-size: 0.813rem; + color: var(--text-tertiary); +} + +.auth-footer-text a, +.auth-footer-link { + color: var(--auth-cta-orange); + text-decoration: none; + font-weight: 500; + cursor: pointer; +} + +.auth-footer-text a:hover, +.auth-footer-link:hover { + text-decoration: underline; +} + +.auth-footer-link--blue { + color: var(--auth-cta-blue); +} + +.auth-back-link { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.813rem; + color: var(--text-tertiary); + text-decoration: none; + transition: color 0.2s; + align-self: flex-start; +} + +.auth-back-link:hover { + color: var(--text-primary); +} + +.auth-back-link svg { + width: 14px; + height: 14px; + background: none !important; +} + +/* Timer */ +.auth-timer { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.813rem; + color: var(--text-tertiary); +} + +.auth-timer svg { + width: 14px; + height: 14px; + background: none !important; +} + +.auth-timer-value { + color: var(--auth-cta-orange); + font-weight: 600; +} + +/* EN: Highlighted email in confirmations */ +/* VI: Email highlight trong xác nhận */ +.auth-highlight { + color: var(--auth-cta-orange); + font-weight: 500; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 12. ROLE HINTS — Staff login role section + ═════════════════════════════════════════════════════════════════════════ */ +.auth-role-hints { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding-top: 40px; +} + +.auth-role-hints-title { + font-size: 0.75rem; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.auth-role-list { + display: flex; + gap: 16px; + align-items: center; +} + +.auth-role-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.813rem; + color: var(--text-secondary); +} + +.auth-role-item svg { + width: 16px; + height: 16px; + background: none !important; + color: var(--text-tertiary); +} + +/* EN: Disclaimer text */ +/* VI: Ghi chú điều khoản */ +.auth-disclaimer { + font-size: 0.688rem; + color: var(--text-disabled); + text-align: center; + line-height: 1.5; + max-width: 360px; + margin-top: 8px; +} + +.auth-disclaimer a { + color: var(--text-tertiary); + text-decoration: underline; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 13. RESPONSIVE — Tablet & Mobile + ═════════════════════════════════════════════════════════════════════════ */ + +/* EN: Tablet (768px) */ +/* VI: Tablet (768px) */ +@media (max-width: 768px) { + .auth-page--split { + flex-direction: column; + } + + .auth-brand-panel--orange, + .auth-brand-panel--dark { + width: 100%; + min-height: auto; + padding: 40px 24px; + } + + .auth-brand-stats { + gap: 24px; + } + + .auth-brand-stat-value { + font-size: 1.25rem; + } + + .auth-brand-features { + display: none; + } + + .auth-form-panel { + min-height: auto; + padding: 40px 24px; + } + + .auth-card { + padding: 32px; + } +} + +/* EN: Mobile (480px) */ +/* VI: Di động (480px) */ +@media (max-width: 480px) { + .auth-page { + padding: 16px; + } + + .auth-page--split { + flex-direction: column; + } + + .auth-brand-panel--orange, + .auth-brand-panel--dark { + width: 100%; + min-height: auto; + padding: 24px 20px; + flex-direction: row; + gap: 12px; + justify-content: flex-start; + } + + .auth-brand-logo { + width: 40px; + height: 40px; + border-radius: 10px; + font-size: 1.125rem; + margin-bottom: 0; + } + + .auth-brand-title { + font-size: 1rem; + text-align: left; + margin: 0; + } + + .auth-brand-desc, + .auth-brand-stats, + .auth-brand-features { + display: none; + } + + .auth-form-panel { + padding: 24px 20px; + } + + .auth-card { + padding: 24px; + border-radius: 16px; + gap: 24px; + } + + .auth-heading { + font-size: 1.375rem; + } + + .auth-icon { + width: 56px; + height: 56px; + } + + .auth-otp-input { + width: 44px; + height: 44px; + font-size: 1.125rem; + } + + .auth-role-list { + flex-wrap: wrap; + justify-content: center; + gap: 10px; + } + + .auth-social-btn { + width: 48px; + height: 48px; + } +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html index 5ef59eb3..2aafffaf 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html @@ -30,6 +30,7 @@ +