Migrate
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
@*
|
||||
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 || Loading)"
|
||||
aria-label="@AriaLabel"
|
||||
aria-busy="@(Loading ? "true" : "false")"
|
||||
aria-disabled="@((Disabled || Loading) ? "true" : "false")"
|
||||
@onclick="OnClick">
|
||||
@if (Loading)
|
||||
{
|
||||
<span class="spinner-small" aria-hidden="true"></span>
|
||||
}
|
||||
@if (IconName != null)
|
||||
{
|
||||
<i data-lucide="@IconName" aria-hidden="true"></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: Accessible label for screen readers (required when button has no visible text).
|
||||
/// VI: Nhãn accessible cho screen reader (bắt buộc khi button không có text hiển thị).
|
||||
/// </summary>
|
||||
[Parameter] public string? AriaLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Button content.
|
||||
/// VI: Nội dung button.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
157
microservices/packages/blazor-ui/Components/Auth/AuthCard.razor
Normal file
157
microservices/packages/blazor-ui/Components/Auth/AuthCard.razor
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
@*
|
||||
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"
|
||||
role="group"
|
||||
aria-label="@GroupAriaLabel">
|
||||
@for (int i = 0; i < DigitCount; i++)
|
||||
{
|
||||
var index = i;
|
||||
<input id="otp-@index"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="1"
|
||||
autocomplete="one-time-code"
|
||||
aria-label="@($"Digit {index + 1} of {DigitCount}")"
|
||||
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: Accessible group label for screen readers.
|
||||
/// VI: Nhãn nhóm accessible cho screen reader.
|
||||
/// </summary>
|
||||
[Parameter] public string GroupAriaLabel { get; set; } = "Enter OTP code";
|
||||
|
||||
/// <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("focusOtpInput", index + 1);
|
||||
}
|
||||
|
||||
// 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("focusOtpInput", index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@*
|
||||
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">
|
||||
@* EN: Google — official multi-color "G" logo *@
|
||||
<button type="button" class="auth-social-btn auth-social-btn--google" title="Google" @onclick="OnGoogleClick">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18A11.96 11.96 0 0 0 1 12c0 1.94.46 3.77 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@* EN: Apple — official Apple logo *@
|
||||
<button type="button" class="auth-social-btn auth-social-btn--apple" title="Apple" @onclick="OnAppleClick">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.52-3.23 0-1.44.62-2.2.44-3.06-.4C3.79 16.17 4.36 9.53 8.76 9.28c1.25.07 2.12.72 2.88.76.99-.2 1.94-.78 3-.84 1.42-.1 2.5.42 3.2 1.33-2.92 1.69-2.23 5.42.53 6.46-.62 1.6-1.42 3.19-2.95 3.29h-.37zM12 9.22c-.12-2.24 1.73-4.25 3.87-4.22.32 2.36-2.12 4.38-3.87 4.22z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@* EN: Facebook — official F logo *@
|
||||
<button type="button" class="auth-social-btn auth-social-btn--facebook" title="Facebook" @onclick="OnFacebookClick">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (ShowZalo)
|
||||
{
|
||||
@* EN: Zalo — Z letter in brand blue *@
|
||||
<button type="button" class="auth-social-btn auth-social-btn--zalo" title="Zalo" @onclick="OnZaloClick">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.373 0 0 4.925 0 11s5.373 11 12 11c1.125 0 2.215-.152 3.253-.437l3.694 1.327a.75.75 0 0 0 .966-.932l-1.152-3.572C22.018 16.037 24 13.696 24 11 24 4.925 18.627 0 12 0zm-3.2 14.4H5.6V13h2.08l-2.48-3.76V8h3.6v1.4H7.12l2.48 3.76v1.24zm5.4-.8c0 .44-.36.8-.8.8h-2.8c-.44 0-.8-.36-.8-.8V8.8c0-.44.36-.8.8-.8h2.8c.44 0 .8.36.8.8v4.8zm3.2.8h-1.6V8h1.6v6.4z"/>
|
||||
</svg>
|
||||
</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 OnFacebookClick { get; set; }
|
||||
[Parameter] public EventCallback OnZaloClick { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
@using System.Globalization
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<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 culture = CultureInfo.CurrentUICulture.Name;
|
||||
return culture.StartsWith("vi", StringComparison.OrdinalIgnoreCase) ? "VI" : "EN";
|
||||
}
|
||||
|
||||
private async Task SwitchLanguage(string targetCulture)
|
||||
{
|
||||
// Save to localStorage for persistence across page reloads
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "aPOS_culture", targetCulture);
|
||||
|
||||
// Force full page reload to reinitialize the WASM app with new culture
|
||||
var currentUri = Navigation.Uri;
|
||||
var uri = new Uri(currentUri);
|
||||
|
||||
// Build clean URL (strip any culture segments from path)
|
||||
var path = uri.AbsolutePath;
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Remove existing culture segment if present
|
||||
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 = segments.Skip(1).ToArray();
|
||||
}
|
||||
|
||||
var cleanPath = "/" + string.Join('/', segments);
|
||||
if (cleanPath == "/") cleanPath = "";
|
||||
|
||||
// Navigate to root with forceLoad to reinitialize WASM
|
||||
Navigation.NavigateTo($"{cleanPath}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// <auto-generated>
|
||||
// EN: This file is auto-generated by the Style Dictionary pipeline.
|
||||
// Do NOT edit manually. Update tokens/*.json and re-run the build.
|
||||
// VI: File này được tạo tự động bởi Style Dictionary pipeline.
|
||||
// KHÔNG chỉnh sửa thủ công. Cập nhật tokens/*.json và build lại.
|
||||
// </auto-generated>
|
||||
|
||||
namespace GoodGo.BlazorUi.DesignTokens;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Auto-generated design token constants. Maps 1:1 with CSS custom properties.
|
||||
/// VI: Hằng số design token tự động. Ánh xạ 1:1 với CSS custom properties.
|
||||
/// </summary>
|
||||
public static class DesignTokens
|
||||
{
|
||||
public const string BorderRadiusBase = "6px"; // Default border radius
|
||||
public const string BorderRadiusLg = "10px"; // Large border radius
|
||||
public const string BorderRadiusXl = "14px"; // XL border radius
|
||||
public const string BorderRadius2xl = "20px"; // 2XL border radius
|
||||
public const string ColorPrimitiveNeutral0 = "#ffffff"; // Pure white
|
||||
public const string ColorPrimitiveNeutral50 = "#fafafa"; // Near white
|
||||
public const string ColorPrimitiveNeutral100 = "#ADADB0"; // Light gray
|
||||
public const string ColorPrimitiveNeutral200 = "#8B8B90"; // Medium gray
|
||||
public const string ColorPrimitiveNeutral300 = "#6B6B70"; // Muted gray
|
||||
public const string ColorPrimitiveNeutral400 = "#3A3A3E"; // Dark gray
|
||||
public const string ColorPrimitiveNeutral500 = "#2A2A2E"; // Darker gray
|
||||
public const string ColorPrimitiveNeutral600 = "#1F1F23"; // Surface border
|
||||
public const string ColorPrimitiveNeutral700 = "#1A1A1D"; // Elevated surface
|
||||
public const string ColorPrimitiveNeutral800 = "#111113"; // Base surface
|
||||
public const string ColorPrimitiveNeutral900 = "#0A0A0B"; // Page background
|
||||
public const string ColorPrimitiveNeutral950 = "#050506"; // Near black
|
||||
public const string ColorPrimitiveAccent400 = "#FF8A4C"; // Orange light
|
||||
public const string ColorPrimitiveAccent500 = "#FF5C00"; // Orange primary
|
||||
public const string ColorPrimitiveAccent600 = "#E05200"; // Orange dark
|
||||
public const string ColorPrimitiveSuccess500 = "#22C55E"; // Success green
|
||||
public const string ColorPrimitiveWhite = "#ffffff";
|
||||
public const string ColorPrimitiveBlack = "#000000";
|
||||
public const string ColorPrimitiveOverlay = "rgba(0, 0, 0, 0.6)";
|
||||
public const string ColorSemanticBgPage = "#0A0A0B"; // Page background
|
||||
public const string ColorSemanticBgSurface = "#111113"; // Card / panel surface
|
||||
public const string ColorSemanticBgElevated = "#1A1A1D"; // Elevated elements
|
||||
public const string ColorSemanticBgInteractive = "#2A2A2E"; // Interactive backgrounds
|
||||
public const string ColorSemanticBgSurfaceHover = "#1F1F23"; // Hover state
|
||||
public const string ColorSemanticBgOverlay = "rgba(10, 10, 11, 0.9)"; // Modal overlay
|
||||
public const string ColorSemanticTextPrimary = "#ffffff"; // Primary text
|
||||
public const string ColorSemanticTextSecondary = "#ADADB0"; // Secondary text
|
||||
public const string ColorSemanticTextTertiary = "#8B8B90"; // Tertiary / helper text
|
||||
public const string ColorSemanticTextDisabled = "#6B6B70"; // Disabled text
|
||||
public const string ColorSemanticTextMuted = "rgba(255, 255, 255, 0.8)"; // Muted white text
|
||||
public const string ColorSemanticTextInverse = "#0A0A0B"; // Inverse (dark on light)
|
||||
public const string ColorSemanticAccentPrimary = "#FF5C00"; // Brand orange
|
||||
public const string ColorSemanticAccentLight = "#FF8A4C"; // Light orange
|
||||
public const string ColorSemanticAccentGlow = "rgba(255, 92, 0, 0.15)"; // Subtle glow
|
||||
public const string ColorSemanticAccentGlowStrong = "rgba(255, 92, 0, 0.3)"; // Strong glow
|
||||
public const string ColorSemanticBorderSubtle = "#1F1F23"; // Subtle divider
|
||||
public const string ColorSemanticBorderDefault = "#2A2A2E"; // Default border
|
||||
public const string ColorSemanticBorderStrong = "#3A3A3E"; // Strong border
|
||||
public const string ColorSemanticActionPrimaryBg = "#FF5C00"; // CTA button bg
|
||||
public const string ColorSemanticActionPrimaryBgHover = "#E05200"; // CTA button hover
|
||||
public const string ColorSemanticActionPrimaryText = "#ffffff"; // CTA button text
|
||||
public const string ColorSemanticActionSecondaryBg = "transparent"; // Ghost button bg
|
||||
public const string ColorSemanticActionSecondaryBgHover = "rgba(255, 255, 255, 0.05)"; // Ghost button hover
|
||||
public const string ColorSemanticActionSecondaryText = "#ffffff"; // Ghost button text
|
||||
public const string ColorSemanticActionSecondaryBorder = "#2A2A2E"; // Ghost button border
|
||||
public const string ColorSemanticStatusSuccess = "#22C55E"; // Success state
|
||||
public const string Spacing1 = "0.25rem"; // 4px
|
||||
public const string Spacing2 = "0.5rem"; // 8px
|
||||
public const string Spacing3 = "0.75rem"; // 12px
|
||||
public const string Spacing4 = "1rem"; // 16px
|
||||
public const string Spacing5 = "1.25rem"; // 20px
|
||||
public const string Spacing6 = "1.5rem"; // 24px
|
||||
public const string Spacing8 = "2rem"; // 32px
|
||||
public const string Spacing10 = "2.5rem"; // 40px
|
||||
public const string Spacing12 = "3rem"; // 48px
|
||||
public const string Spacing16 = "4rem"; // 64px
|
||||
public const string Spacing20 = "5rem"; // 80px
|
||||
public const string Spacing24 = "6rem"; // 96px
|
||||
public const string FontFamilyHeading = "'Roboto', system-ui, sans-serif"; // Heading / display font
|
||||
public const string FontFamilyBody = "'Roboto', system-ui, sans-serif"; // Body text font
|
||||
}
|
||||
26
microservices/packages/blazor-ui/GoodGo.BlazorUi.csproj
Normal file
26
microservices/packages/blazor-ui/GoodGo.BlazorUi.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
EN: Razor Class Library — shared Blazor UI components for GoodGo platform.
|
||||
Consumed by web-client-tpos-net and web-client-base-net.
|
||||
VI: Razor Class Library — thư viện component Blazor dùng chung cho nền tảng GoodGo.
|
||||
Được dùng bởi web-client-tpos-net và web-client-base-net.
|
||||
-->
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AddRazorSupportForMvc>false</AddRazorSupportForMvc>
|
||||
<AssemblyName>GoodGo.BlazorUi</AssemblyName>
|
||||
<RootNamespace>GoodGo.BlazorUi</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
<Description>Shared Blazor UI component library for GoodGo platform</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.2" />
|
||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
microservices/packages/blazor-ui/_Imports.razor
Normal file
10
microservices/packages/blazor-ui/_Imports.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using GoodGo.BlazorUi
|
||||
@using GoodGo.BlazorUi.Components.Auth
|
||||
@using GoodGo.BlazorUi.Components.Common
|
||||
Reference in New Issue
Block a user