feat: Implement comprehensive authentication UI including login, registration, password reset, OTP verification, and supporting components.

This commit is contained in:
Ho Ngoc Hai
2026-02-12 09:17:47 +07:00
parent f678a1a69a
commit db53e1abae
18 changed files with 2206 additions and 0 deletions

View File

@@ -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).
*@
<button type="@ButtonType"
class="auth-btn auth-btn--@Variant @CssClass"
disabled="@Disabled"
@onclick="OnClick">
@if (Loading)
{
<span class="spinner-small"></span>
}
@if (IconName != null)
{
<i data-lucide="@IconName"></i>
}
@ChildContent
</button>
@code {
/// <summary>
/// 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.
/// </summary>
[Parameter] public string Variant { get; set; } = "orange";
/// <summary>
/// EN: HTML button type attribute.
/// VI: Attribute type của button.
/// </summary>
[Parameter] public string ButtonType { get; set; } = "button";
/// <summary>
/// EN: Whether the button is disabled.
/// VI: Button có bị disable không.
/// </summary>
[Parameter] public bool Disabled { get; set; }
/// <summary>
/// EN: Whether to show a loading spinner.
/// VI: Có hiển thị spinner loading không.
/// </summary>
[Parameter] public bool Loading { get; set; }
/// <summary>
/// EN: Optional Lucide icon name.
/// VI: Tên icon Lucide tùy chọn.
/// </summary>
[Parameter] public string? IconName { get; set; }
/// <summary>
/// EN: Additional CSS classes.
/// VI: CSS class bổ sung.
/// </summary>
[Parameter] public string? CssClass { get; set; }
/// <summary>
/// EN: Click event callback.
/// VI: Callback sự kiện click.
/// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
/// <summary>
/// EN: Button content.
/// VI: Nội dung button.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
}

View File

@@ -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ả.
*@
<div class="auth-card @(Compact ? "auth-card--compact" : "") @(Transparent ? "auth-card--transparent" : "") @CssClass">
@if (BackLink != null)
{
<a href="@BackLink" class="auth-back-link">
<i data-lucide="arrow-left"></i> @BackLinkText
</a>
}
@if (ShowHeader)
{
<div class="auth-header">
@if (Icon != null)
{
<div class="auth-icon @IconClass">
<i data-lucide="@Icon"></i>
</div>
}
@if (RoleBadge != null)
{
<span class="auth-role-badge @RoleBadgeClass">@RoleBadge</span>
}
@if (Title != null)
{
<h1 class="auth-heading">@Title</h1>
}
@if (Subtitle != null)
{
<p class="auth-subheading">@Subtitle</p>
}
@if (SecurityHint != null)
{
<div class="auth-security-hint @SecurityHintClass">
<i data-lucide="@SecurityHintIcon"></i>
@SecurityHint
</div>
}
</div>
}
@ChildContent
@if (FooterContent != null)
{
<div class="auth-footer">
@FooterContent
</div>
}
</div>
@code {
/// <summary>
/// EN: Card title text.
/// VI: Tiêu đề card.
/// </summary>
[Parameter] public string? Title { get; set; }
/// <summary>
/// EN: Subtitle/description text shown below the title.
/// VI: Mô tả hiển thị dưới tiêu đề.
/// </summary>
[Parameter] public string? Subtitle { get; set; }
/// <summary>
/// EN: Lucide icon name shown above the title.
/// VI: Tên icon Lucide hiển thị trên tiêu đề.
/// </summary>
[Parameter] public string? Icon { get; set; }
/// <summary>
/// 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).
/// </summary>
[Parameter] public string IconClass { get; set; } = "auth-icon--blue";
/// <summary>
/// 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").
/// </summary>
[Parameter] public string? RoleBadge { get; set; }
/// <summary>
/// EN: CSS class for the role badge.
/// VI: CSS class cho role badge.
/// </summary>
[Parameter] public string RoleBadgeClass { get; set; } = "auth-role-badge--blue";
/// <summary>
/// EN: Security hint message (e.g., "Khu vực bảo mật cao").
/// VI: Thông báo bảo mật.
/// </summary>
[Parameter] public string? SecurityHint { get; set; }
/// <summary>
/// EN: Icon for the security hint.
/// VI: Icon cho thông báo bảo mật.
/// </summary>
[Parameter] public string SecurityHintIcon { get; set; } = "shield-alert";
/// <summary>
/// EN: CSS class for the security hint.
/// VI: CSS class cho thông báo bảo mật.
/// </summary>
[Parameter] public string SecurityHintClass { get; set; } = "auth-security-hint--warning";
/// <summary>
/// EN: Whether to show the header section.
/// VI: Có hiển thị phần header không.
/// </summary>
[Parameter] public bool ShowHeader { get; set; } = true;
/// <summary>
/// EN: Use compact gap (28px vs 32px).
/// VI: Dùng gap nhỏ hơn (28px thay vì 32px).
/// </summary>
[Parameter] public bool Compact { get; set; }
/// <summary>
/// EN: Transparent background (used inside split panels).
/// VI: Nền trong suốt (dùng trong split panel).
/// </summary>
[Parameter] public bool Transparent { get; set; }
/// <summary>
/// EN: Back link URL.
/// VI: URL link quay lại.
/// </summary>
[Parameter] public string? BackLink { get; set; }
/// <summary>
/// EN: Back link text.
/// VI: Text link quay lại.
/// </summary>
[Parameter] public string BackLinkText { get; set; } = "Quay lại đăng nhập";
/// <summary>
/// EN: Additional CSS classes.
/// VI: CSS class bổ sung.
/// </summary>
[Parameter] public string? CssClass { get; set; }
/// <summary>
/// EN: Main content slot.
/// VI: Slot nội dung chính.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>
/// EN: Footer content slot.
/// VI: Slot nội dung footer.
/// </summary>
[Parameter] public RenderFragment? FooterContent { get; set; }
}

View File

@@ -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).
*@
<div class="auth-field">
@if (Label != null || ForgotPasswordLink != null)
{
<div class="auth-label-row">
@if (Label != null)
{
<label class="auth-label" for="@InputId">@Label</label>
}
@if (ForgotPasswordLink != null)
{
<a href="@ForgotPasswordLink" class="auth-label-link">@ForgotPasswordText</a>
}
</div>
}
<div class="auth-input-wrapper">
@if (PrefixIcon != null)
{
<span class="auth-input-icon">
<i data-lucide="@PrefixIcon"></i>
</span>
}
<input id="@InputId"
type="@_currentType"
class="auth-input @(PrefixIcon == null ? "auth-input--no-icon" : "")"
placeholder="@Placeholder"
value="@Value"
autocomplete="@AutoComplete"
@oninput="OnInput"
@onchange="OnChange" />
@if (InputType == "password")
{
<button type="button" class="auth-input-action" @onclick="TogglePassword" title="Toggle password visibility">
<i data-lucide="@(_showPassword ? "eye-off" : "eye")"></i>
</button>
}
</div>
</div>
@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<ChangeEventArgs> 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;
}
}

View File

@@ -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ê.
*@
<div class="auth-brand-panel @PanelClass">
<div class="auth-brand-logo">@LogoText</div>
<h2 class="auth-brand-title">@Title</h2>
<p class="auth-brand-desc">@Description</p>
@if (ShowStats)
{
<div class="auth-brand-stats">
@foreach (var stat in Stats)
{
<div class="auth-brand-stat">
<span class="auth-brand-stat-value">@stat.Value</span>
<span class="auth-brand-stat-label">@stat.Label</span>
</div>
}
</div>
}
@if (Features != null && Features.Count > 0)
{
<div class="auth-brand-features">
@foreach (var feature in Features)
{
<div class="auth-brand-feature">
<i data-lucide="check"></i>
<span>@feature</span>
</div>
}
</div>
}
@ChildContent
</div>
@code {
/// <summary>
/// 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).
/// </summary>
[Parameter] public string PanelClass { get; set; } = "auth-brand-panel--orange";
/// <summary>
/// EN: Logo character/text displayed in the brand panel.
/// VI: Ký tự/text logo hiển thị trong brand panel.
/// </summary>
[Parameter] public string LogoText { get; set; } = "a";
/// <summary>
/// EN: Brand panel title.
/// VI: Tiêu đề brand panel.
/// </summary>
[Parameter] public string Title { get; set; } = "aPOS Branch";
/// <summary>
/// EN: Description text.
/// VI: Text mô tả.
/// </summary>
[Parameter] public string Description { get; set; } = "";
/// <summary>
/// EN: Whether to show trust stats.
/// VI: Có hiển thị thống kê tin cậy không.
/// </summary>
[Parameter] public bool ShowStats { get; set; } = true;
/// <summary>
/// EN: Trust statistics data.
/// VI: Dữ liệu thống kê tin cậy.
/// </summary>
[Parameter] public List<BrandStat> Stats { get; set; } = new()
{
new("1,200+", "Cửa hàng"),
new("50K+", "Giao dịch/ngày"),
new("99.9%", "Uptime")
};
/// <summary>
/// EN: Feature list items.
/// VI: Danh sách tính năng.
/// </summary>
[Parameter] public List<string>? Features { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>
/// EN: Brand stat record.
/// VI: Record thống kê thương hiệu.
/// </summary>
public record BrandStat(string Value, string Label);
}

View File

@@ -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
<div class="auth-otp-group">
@for (int i = 0; i < DigitCount; i++)
{
var index = i;
<input id="otp-@index"
type="text"
inputmode="numeric"
maxlength="1"
class="auth-otp-input @(UseBlueTheme ? "auth-otp-input--blue" : "") @(!string.IsNullOrEmpty(_digits[index]) ? "auth-otp-input--filled" : "")"
value="@_digits[index]"
@oninput="(e) => HandleInput(e, index)"
@onkeydown="(e) => HandleKeyDown(e, index)" />
}
</div>
@code {
private string[] _digits = new string[6];
/// <summary>
/// EN: Number of OTP digits.
/// VI: Số chữ số OTP.
/// </summary>
[Parameter] public int DigitCount { get; set; } = 6;
/// <summary>
/// EN: Use blue theme (for 2FA authenticator).
/// VI: Dùng theme xanh dương (cho 2FA authenticator).
/// </summary>
[Parameter] public bool UseBlueTheme { get; set; }
/// <summary>
/// EN: Callback when all digits are entered.
/// VI: Callback khi tất cả chữ số được nhập.
/// </summary>
[Parameter] public EventCallback<string> 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()");
}
}
}

View File

@@ -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.
*@
<div class="auth-divider">@DividerText</div>
<div class="auth-social-group">
<button type="button" class="auth-social-btn auth-social-btn--google" title="Đăng nhập với Google" @onclick="OnGoogleClick">
G
</button>
<button type="button" class="auth-social-btn auth-social-btn--apple" title="Đăng nhập với Apple" @onclick="OnAppleClick">
</button>
@if (ShowZalo)
{
<button type="button" class="auth-social-btn auth-social-btn--zalo" title="Đăng nhập với Zalo" @onclick="OnZaloClick">
Z
</button>
}
</div>
@code {
/// <summary>
/// EN: Divider text between form and social buttons.
/// VI: Text divider giữa form và social buttons.
/// </summary>
[Parameter] public string DividerText { get; set; } = "Hoặc đăng nhập bằng";
/// <summary>
/// EN: Whether to show Zalo button (Vietnam market).
/// VI: Có hiển thị nút Zalo không (thị trường VN).
/// </summary>
[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; }
}

View File

@@ -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
*@
<PageTitle>Kiểm tra email của bạn</PageTitle>
<div class="auth-page">
<AuthCard Title="Kiểm tra email của bạn"
Subtitle="Chúng tôi đã gửi đường dẫn xác nhận đến email của bạn:"
Icon="mail-check"
IconClass="auth-icon--success"
Compact="true">
<ChildContent>
<span class="auth-highlight">u****a@gmail.com</span>
<AuthButton Variant="orange" IconName="external-link">
Mở ứng dụng Email
</AuthButton>
<AuthButton Variant="outline" IconName="mail">
Mở ứng dụng Email
</AuthButton>
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Không nhận được email?
</span>
<span class="auth-footer-text">
Kiểm tra spam hoặc <a href="#">gửi lại</a>
</span>
<a href="/auth/login/branch" class="auth-back-link" style="align-self: center;">
<i data-lucide="arrow-left"></i> Quay lại đăng nhập
</a>
</FooterContent>
</AuthCard>
</div>
@code {
}

View File

@@ -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
*@
<PageTitle>Quên mật khẩu</PageTitle>
<div class="auth-page">
@if (!_resetSent)
{
<AuthCard Title="Quên mật khẩu?"
Subtitle="Nhập email hoặc số điện thoại để nhận đường dẫn đặt lại mật khẩu."
Icon="key-round"
IconClass="auth-icon--orange"
BackLink="/auth/login/branch"
BackLinkText="Quay lại đăng nhập"
Compact="true">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Email hoặc số điện thoại"
InputType="text"
Placeholder="Nhập email hoặc số điện thoại"
PrefixIcon="mail"
InputId="forgot-email" />
</div>
<AuthButton Variant="orange" OnClick="HandleSubmit">
Gửi yêu cầu
</AuthButton>
</ChildContent>
</AuthCard>
}
else
{
<AuthCard Title="Kiểm tra hộp thư"
Subtitle="@($"Chúng tôi đã gửi đường dẫn đặt lại mật khẩu đến:")"
Icon="mail-check"
IconClass="auth-icon--success">
<ChildContent>
<span class="auth-highlight">user@example.com</span>
<AuthButton Variant="orange" IconName="external-link">
Mở ứng dụng email
</AuthButton>
<AuthButton Variant="outline">
Gửi lại email
</AuthButton>
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Không nhận được email?
</span>
<span class="auth-footer-text">
Kiểm tra spam hoặc <a href="/auth/forgot-password-new">thử lại với số khác</a>
</span>
</FooterContent>
</AuthCard>
}
</div>
@code {
private bool _resetSent = false;
private void HandleSubmit()
{
_resetSent = true;
}
}

View File

@@ -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
*@
<PageTitle>Đăng nhập Admin</PageTitle>
<div class="auth-page">
<AuthCard Title="Đăng nhập Admin"
Subtitle="Truy cập hệ thống quản trị aPOS"
Icon="shield"
IconClass="auth-icon--blue"
RoleBadge="QUẢN TRỊ"
RoleBadgeClass="auth-role-badge--blue"
SecurityHint="Khu vực bảo mật cao"
SecurityHintIcon="shield-alert"
SecurityHintClass="auth-security-hint--warning">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Email quản trị viên"
InputType="email"
Placeholder="admin@company.com"
PrefixIcon="mail"
InputId="admin-email"
AutoComplete="email" />
<AuthInput Label="Mật khẩu"
InputType="password"
Placeholder="••••••••"
PrefixIcon="lock"
InputId="admin-password"
AutoComplete="current-password" />
</div>
<AuthButton Variant="blue" IconName="shield">
Đăng nhập bảo mật
</AuthButton>
</ChildContent>
<FooterContent>
<div class="auth-role-hints">
<span class="auth-role-hints-title">Đăng nhập với vai trò khác</span>
<div class="auth-role-list">
<a href="/auth/login/branch" class="auth-role-item">
<i data-lucide="store"></i> Chi nhánh
</a>
<a href="/auth/login/staff" class="auth-role-item">
<i data-lucide="user"></i> Nhân viên
</a>
</div>
</div>
</FooterContent>
</AuthCard>
</div>
@code {
}

View File

@@ -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
*@
<PageTitle>Đăng nhập Chi nhánh</PageTitle>
<div class="auth-page--split">
<BrandPanel PanelClass="auth-brand-panel--orange"
LogoText="a"
Title="aPOS Branch"
Description="Quản lý và theo dõi chi nhánh của bạn. Truy cập báo cáo doanh thu, quản lý nhân viên và tồn kho hiệu quả." />
<div class="auth-form-panel">
<AuthCard Title="Đăng nhập Chi nhánh"
Subtitle="Quản lý và theo dõi chi nhánh của bạn"
Transparent="true">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Email hoặc số điện thoại"
InputType="text"
Placeholder="branch@company.com"
PrefixIcon="mail"
InputId="branch-email"
AutoComplete="email" />
<AuthInput Label="Mật khẩu"
InputType="password"
Placeholder="••••••••"
PrefixIcon="lock"
InputId="branch-password"
AutoComplete="current-password"
ForgotPasswordLink="/auth/forgot-password"
ForgotPasswordText="Quên mật khẩu?" />
</div>
<AuthButton Variant="orange">
Đăng nhập
</AuthButton>
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Chưa có tài khoản? <a href="/auth/register">Liên hệ</a>
</span>
</FooterContent>
</AuthCard>
</div>
</div>
@code {
}

View File

@@ -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
*@
<PageTitle>Đăng nhập Khách hàng</PageTitle>
<div class="auth-page--split">
<BrandPanel PanelClass="auth-brand-panel--dark"
LogoText="G"
Title="GoodGo"
Description="Mua sắm thông minh, tích điểm đổi quà. Trải nghiệm thanh toán nhanh chóng và tiện lợi."
ShowStats="false"
Features="@_features" />
<div class="auth-form-panel">
<AuthCard Title="Chào mừng bạn!"
Subtitle="Đăng nhập để nhận ưu đãi và tích điểm thưởng"
Transparent="true">
<ChildContent>
<div class="auth-form">
<div class="auth-field">
<label class="auth-label">Số điện thoại</label>
<div class="auth-phone-group">
<span class="auth-phone-prefix">🇻🇳 +84</span>
<input type="tel"
class="auth-phone-input"
placeholder="912 345 678"
id="customer-phone"
autocomplete="tel" />
</div>
</div>
</div>
<AuthButton Variant="orange" IconName="arrow-right">
Nhận mã OTP
</AuthButton>
<SocialLogin DividerText="Hoặc đăng nhập bằng" ShowZalo="true" />
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Chưa có tài khoản? <a href="/auth/register/customer">Đăng ký ngay</a>
</span>
<p class="auth-disclaimer">
Bằng việc tiếp tục, bạn đồng ý với
<a href="/terms">Điều khoản sử dụng</a> và
<a href="/privacy">Chính sách bảo mật</a>
</p>
</FooterContent>
</AuthCard>
</div>
</div>
@code {
private List<string> _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"
};
}

View File

@@ -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
*@
<PageTitle>Đăng nhập Nhân viên</PageTitle>
<div class="auth-page">
<AuthCard Title="Đăng nhập Nhân viên"
Subtitle="Nhập mã nhân viên hoặc email để bắt đầu ca làm việc"
Icon="user-check"
IconClass="auth-icon--green"
RoleBadge="NHÂN VIÊN"
RoleBadgeClass="auth-role-badge--green"
Compact="true">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Mã nhân viên hoặc email"
InputType="text"
Placeholder="NV001 hoặc staff@company.com"
PrefixIcon="user"
InputId="staff-id"
AutoComplete="username" />
<AuthInput Label="Mật khẩu"
InputType="password"
Placeholder="••••••••"
PrefixIcon="lock"
InputId="staff-password"
AutoComplete="current-password" />
</div>
<AuthButton Variant="green" IconName="log-in">
Đăng nhập ca làm việc
</AuthButton>
</ChildContent>
<FooterContent>
<div class="auth-role-hints">
<span class="auth-role-hints-title">Vai trò hỗ trợ</span>
<div class="auth-role-list">
<span class="auth-role-item">
<i data-lucide="monitor"></i> Thu ngân
</span>
<span class="auth-role-item">
<i data-lucide="coffee"></i> Barista
</span>
<span class="auth-role-item">
<i data-lucide="clipboard-list"></i> Phục vụ
</span>
<span class="auth-role-item">
<i data-lucide="settings"></i> Quản lý
</span>
</div>
</div>
<span class="auth-footer-text">
Quên mật khẩu? <a href="/auth/forgot-password">Liên hệ quản lý</a>
</span>
</FooterContent>
</AuthCard>
</div>
@code {
}

View File

@@ -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
*@
<PageTitle>Xác thực OTP</PageTitle>
<div class="auth-page">
<AuthCard Title="Xác thực OTP"
Subtitle="Nhập mã 6 số đã gửi đến điện thoại của bạn *** 679"
Icon="smartphone"
IconClass="auth-icon--green"
Compact="true">
<ChildContent>
<OtpInput OnComplete="HandleOtpComplete" />
<AuthButton Variant="orange">
Xác nhận
</AuthButton>
</ChildContent>
<FooterContent>
<div class="auth-timer">
<i data-lucide="clock"></i>
Mã hết hạn sau: <span class="auth-timer-value">04:30</span>
</div>
<span class="auth-footer-text">
Không nhận được mã? <a href="#">Gửi lại</a>
</span>
</FooterContent>
</AuthCard>
</div>
@code {
private async Task HandleOtpComplete(string code)
{
Console.WriteLine($"OTP entered: {code}");
}
}

View File

@@ -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
*@
<PageTitle>Đặt lại mật khẩu</PageTitle>
<div class="auth-page">
@if (!_resetSuccess)
{
<AuthCard Title="Đặt lại mật khẩu"
Subtitle="Tạo mật khẩu mới cho tài khoản của bạn"
Icon="lock"
IconClass="auth-icon--blue"
BackLink="/auth/forgot-password-new"
BackLinkText="Quay lại"
Compact="true">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Mật khẩu mới"
InputType="password"
Placeholder="Tối thiểu 8 ký tự"
PrefixIcon="lock"
InputId="new-password" />
<AuthInput Label="Xác nhận mật khẩu"
InputType="password"
Placeholder="Nhập lại mật khẩu"
PrefixIcon="lock"
InputId="confirm-password" />
<div class="auth-strength">
<span class="auth-strength-title">Yêu cầu mật khẩu</span>
<div class="auth-strength-item auth-strength-item--pass">
<i data-lucide="check-circle"></i> Ít nhất 8 ký tự
</div>
<div class="auth-strength-item auth-strength-item--pass">
<i data-lucide="check-circle"></i> Chứa chữ hoa và chữ thường
</div>
<div class="auth-strength-item">
<i data-lucide="circle"></i> Chứa ít nhất 1 số hoặc 1 ký tự đặc biệt
</div>
</div>
</div>
<AuthButton Variant="orange" OnClick="HandleReset">
Đặt lại mật khẩu
</AuthButton>
</ChildContent>
</AuthCard>
}
else
{
<AuthCard Title="Đặt lại thành công!"
Subtitle="Mật khẩu của bạn đã được cập nhật. Bạn có thể đăng nhập với mật khẩu mới."
Icon="check-circle"
IconClass="auth-icon--success">
<ChildContent>
<AuthButton Variant="orange" OnClick="GoToLogin">
Đăng nhập ngay
</AuthButton>
</ChildContent>
</AuthCard>
}
</div>
@code {
private bool _resetSuccess = false;
private void HandleReset()
{
_resetSuccess = true;
}
private void GoToLogin()
{
Navigation.NavigateTo("/auth/login/branch");
}
}

View File

@@ -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
*@
<PageTitle>Đăng ký tài khoản</PageTitle>
<div class="auth-page--split">
<BrandPanel PanelClass="auth-brand-panel--dark"
LogoText="G"
Title="Tham gia GoodGo"
Description="Tạo tài khoản để tích điểm, nhận ưu đãi độc quyền và theo dõi đơn hàng dễ dàng."
ShowStats="false"
Features="@_features" />
<div class="auth-form-panel">
<AuthCard Title="Tạo tài khoản"
Subtitle="Điền thông tin để bắt đầu tích điểm và nhận ưu đãi"
Transparent="true">
<ChildContent>
<div class="auth-form">
<AuthInput Label="Họ và tên"
InputType="text"
Placeholder="Nguyễn Văn A"
PrefixIcon="user"
InputId="reg-name"
AutoComplete="name" />
<div class="auth-field">
<label class="auth-label">Số điện thoại</label>
<div class="auth-phone-group">
<span class="auth-phone-prefix">🇻🇳 +84</span>
<input type="tel"
class="auth-phone-input"
placeholder="912 345 678"
id="reg-phone"
autocomplete="tel" />
</div>
</div>
<AuthInput Label="Email (tuỳ chọn)"
InputType="email"
Placeholder="email@example.com"
PrefixIcon="mail"
InputId="reg-email"
AutoComplete="email" />
<div class="auth-checkbox-row">
<input type="checkbox" id="reg-terms" />
<label for="reg-terms">
Tôi đồng ý với <a href="/terms">Điều khoản sử dụng</a>
và <a href="/privacy">Chính sách bảo mật</a>
</label>
</div>
</div>
<AuthButton Variant="orange" IconName="user-plus">
Đăng ký
</AuthButton>
<SocialLogin DividerText="Hoặc đăng ký bằng" ShowZalo="true" />
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Đã có tài khoản? <a href="/auth/login/customer">Đăng nhập</a>
</span>
</FooterContent>
</AuthCard>
</div>
</div>
@code {
private List<string> _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"
};
}

View File

@@ -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
*@
<PageTitle>Xác thực 2 lớp</PageTitle>
<div class="auth-page">
<AuthCard Title="Xác thực 2 lớp"
Subtitle="Nhập mã 6 số từ ứng dụng Google Authenticator hoặc Authy"
Icon="shield-check"
IconClass="auth-icon--blue"
Compact="true">
<ChildContent>
<OtpInput UseBlueTheme="true" OnComplete="Handle2FAComplete" />
<AuthButton Variant="blue">
Xác nhận
</AuthButton>
</ChildContent>
<FooterContent>
<span class="auth-footer-text">
Không thể sử dụng Authenticator? <a href="#" class="auth-footer-link--blue">Dùng backup code</a>
</span>
</FooterContent>
</AuthCard>
</div>
@code {
private async Task Handle2FAComplete(string code)
{
Console.WriteLine($"2FA code entered: {code}");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
<!-- EN: Custom CSS -->
<!-- VI: CSS tùy chỉnh -->
<link rel="stylesheet" href="/css/app.css" />
<link rel="stylesheet" href="/css/auth.css" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link href="/WebClientTpos.Client.styles.css" rel="stylesheet" />
</head>