feat: Introduce dedicated Blazor pages and DTOs for user authentication and account management, updating localization and styling.
This commit is contained in:
@@ -1,201 +0,0 @@
|
||||
@page "/auth"
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@*
|
||||
EN: Authentication page with Register and Login forms.
|
||||
VI: Trang xác thực với form Đăng ký và Đăng nhập.
|
||||
*@
|
||||
|
||||
<PageTitle>Authentication / Xác thực</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<div class="auth-tabs">
|
||||
<button class="tab-btn @(activeTab == "login" ? "active" : "")" @onclick="@(() => activeTab = "login")">
|
||||
Login / Đăng nhập
|
||||
</button>
|
||||
<button class="tab-btn @(activeTab == "register" ? "active" : "")" @onclick="@(() => activeTab = "register")">
|
||||
Register / Đăng ký
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════════════════
|
||||
EN: Login Form
|
||||
VI: Form đăng nhập
|
||||
═══════════════════════════════════════════════════════════════════════════════ *@
|
||||
@if (activeTab == "login")
|
||||
{
|
||||
<section class="glass-card auth-card">
|
||||
<h2>Welcome Back / Chào mừng trở lại</h2>
|
||||
<p class="subtitle">Sign in to your account / Đăng nhập vào tài khoản</p>
|
||||
|
||||
<EditForm Model="@loginModel" OnValidSubmit="HandleLogin" FormName="Login">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="login-email">Email *</label>
|
||||
<InputText id="login-email" @bind-Value="loginModel.Email" class="form-input" placeholder="email@example.com" />
|
||||
<ValidationMessage For="() => loginModel.Email" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="login-password">Password / Mật khẩu *</label>
|
||||
<InputText id="login-password" @bind-Value="loginModel.Password" type="password" class="form-input" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => loginModel.Password" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<InputCheckbox id="remember-me" @bind-Value="loginModel.RememberMe" class="form-checkbox" />
|
||||
<label for="remember-me">Remember me / Ghi nhớ đăng nhập</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner"></span>
|
||||
}
|
||||
Sign In / Đăng nhập
|
||||
</button>
|
||||
</EditForm>
|
||||
</section>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════════════════
|
||||
EN: Register Form
|
||||
VI: Form đăng ký
|
||||
═══════════════════════════════════════════════════════════════════════════════ *@
|
||||
@if (activeTab == "register")
|
||||
{
|
||||
<section class="glass-card auth-card">
|
||||
<h2>Create Account / Tạo tài khoản</h2>
|
||||
<p class="subtitle">Join us today / Tham gia với chúng tôi</p>
|
||||
|
||||
<EditForm Model="@registerModel" OnValidSubmit="HandleRegister" FormName="Register">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary class="validation-summary" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-name">Display Name / Tên hiển thị *</label>
|
||||
<InputText id="reg-name" @bind-Value="registerModel.DisplayName" class="form-input" placeholder="John Doe" />
|
||||
<ValidationMessage For="() => registerModel.DisplayName" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-email">Email *</label>
|
||||
<InputText id="reg-email" @bind-Value="registerModel.Email" class="form-input" placeholder="email@example.com" />
|
||||
<ValidationMessage For="() => registerModel.Email" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-password">Password / Mật khẩu *</label>
|
||||
<InputText id="reg-password" @bind-Value="registerModel.Password" type="password" class="form-input" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => registerModel.Password" class="validation-message" />
|
||||
<small class="form-hint">Min 8 chars, uppercase, lowercase, digit / Tối thiểu 8 ký tự, chữ hoa, chữ thường, số</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-confirm">Confirm Password / Xác nhận mật khẩu *</label>
|
||||
<InputText id="reg-confirm" @bind-Value="registerModel.ConfirmPassword" type="password" class="form-input" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => registerModel.ConfirmPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner"></span>
|
||||
}
|
||||
Create Account / Tạo tài khoản
|
||||
</button>
|
||||
</EditForm>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string activeTab = "login";
|
||||
private LoginDto loginModel = new();
|
||||
private RegisterDto registerModel = new();
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/Auth/login", loginModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
|
||||
if (result?.Success == true)
|
||||
{
|
||||
success = true;
|
||||
message = $"Welcome back, {result.Data?.DisplayName}! / Chào mừng trở lại, {result.Data?.DisplayName}!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = "Login failed. Please check your credentials. / Đăng nhập thất bại.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRegister()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/Auth/register", registerModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
|
||||
if (result?.Success == true)
|
||||
{
|
||||
success = true;
|
||||
message = "Account created successfully! / Tạo tài khoản thành công!";
|
||||
activeTab = "login";
|
||||
registerModel = new();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
message = $"Registration failed: {content}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
@page "/forgot-password"
|
||||
@using WebClientBase.Shared.DTOs
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject IStringLocalizer<ForgotPassword> L
|
||||
|
||||
@*
|
||||
EN: Forgot password page.
|
||||
VI: Trang quên mật khẩu.
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_ForgotPassword_Title"]</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<section class="auth-card">
|
||||
<h1 class="auth-title">@L["Auth_ForgotPassword_Title"]</h1>
|
||||
<p class="auth-subtitle">@L["Auth_ForgotPassword_Subtitle"]</p>
|
||||
|
||||
<EditForm Model="@forgotPasswordModel" OnValidSubmit="HandleForgotPassword" FormName="ForgotPasswordForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="forgot-email">@L["Auth_ForgotPassword_Email"] *</label>
|
||||
<InputText id="forgot-email"
|
||||
@bind-Value="forgotPasswordModel.Email"
|
||||
class="form-input"
|
||||
placeholder="email@example.com"
|
||||
autocomplete="email" />
|
||||
<ValidationMessage For="() => forgotPasswordModel.Email" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner-small"></span>
|
||||
<span>@L["Common_Loading"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_ForgotPassword_Submit"]
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="auth-footer">
|
||||
<a href="/login" class="link-primary">@L["Auth_ForgotPassword_BackToLogin"]</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ForgotPasswordDto forgotPasswordModel = new();
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle forgot password form submission.
|
||||
/// VI: Xử lý submit form quên mật khẩu.
|
||||
/// </summary>
|
||||
private async Task HandleForgotPassword()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/auth/forgot-password", forgotPasswordModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||
if (result?.Success == true)
|
||||
{
|
||||
success = true;
|
||||
message = L["Auth_ForgotPassword_Success"];
|
||||
forgotPasswordModel = new(); // Clear form
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = result?.Error ?? L["Auth_ForgotPassword_Error"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_ForgotPassword_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
@page "/login"
|
||||
@using WebClientBase.Shared.DTOs
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IStringLocalizer<Login> L
|
||||
|
||||
@*
|
||||
EN: Login page with email/password authentication.
|
||||
VI: Trang đăng nhập với xác thực email/mật khẩu.
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_Login_Title"]</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<section class="auth-card">
|
||||
<h1 class="auth-title">@L["Auth_Login_Title"]</h1>
|
||||
<p class="auth-subtitle">@L["Auth_Login_Subtitle"]</p>
|
||||
|
||||
<EditForm Model="@loginModel" OnValidSubmit="HandleLogin" FormName="LoginForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="login-email">@L["Auth_Login_Email"] *</label>
|
||||
<InputText id="login-email"
|
||||
@bind-Value="loginModel.Email"
|
||||
class="form-input"
|
||||
placeholder="email@example.com"
|
||||
autocomplete="email" />
|
||||
<ValidationMessage For="() => loginModel.Email" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="login-password">@L["Auth_Login_Password"] *</label>
|
||||
<InputText id="login-password"
|
||||
@bind-Value="loginModel.Password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••"
|
||||
autocomplete="current-password" />
|
||||
<ValidationMessage For="() => loginModel.Password" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-actions-row">
|
||||
<div class="checkbox-group">
|
||||
<InputCheckbox id="remember-me"
|
||||
@bind-Value="loginModel.RememberMe"
|
||||
class="form-checkbox" />
|
||||
<label for="remember-me" class="checkbox-label">@L["Auth_Login_RememberMe"]</label>
|
||||
</div>
|
||||
|
||||
<a href="/forgot-password" class="link-secondary">@L["Auth_Login_ForgotPassword"]</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner-small"></span>
|
||||
<span>@L["Common_Loading"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_Login_Submit"]
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="auth-footer">
|
||||
<span>@L["Auth_Login_NoAccount"]</span>
|
||||
<a href="/register" class="link-primary">@L["Auth_Login_RegisterLink"]</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private LoginDto loginModel = new();
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle login form submission.
|
||||
/// VI: Xử lý submit form đăng nhập.
|
||||
/// </summary>
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/auth/login", loginModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
|
||||
if (result?.Success == true && result.Data != null)
|
||||
{
|
||||
success = true;
|
||||
message = string.Format(L["Auth_Login_Success"], result.Data.DisplayName);
|
||||
|
||||
// EN: Redirect to home after 1 second
|
||||
// VI: Chuyển hướng về trang chủ sau 1 giây
|
||||
await Task.Delay(1000);
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_Login_Error"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_Login_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
@page "/profile"
|
||||
@using WebClientBase.Shared.DTOs
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IStringLocalizer<Profile> L
|
||||
|
||||
@*
|
||||
EN: User profile management page (requires authentication).
|
||||
VI: Trang quản lý hồ sơ người dùng (yêu cầu xác thực).
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_Profile_Title"]</PageTitle>
|
||||
|
||||
<div class="profile-container">
|
||||
<div class="profile-header">
|
||||
<h1 class="profile-title">@L["Auth_Profile_Title"]</h1>
|
||||
<p class="profile-subtitle">@L["Auth_Profile_Subtitle"]</p>
|
||||
</div>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="loading-state">
|
||||
<span class="spinner"></span>
|
||||
<p>@L["Common_Loading"]</p>
|
||||
</div>
|
||||
}
|
||||
else if (userProfile != null)
|
||||
{
|
||||
<div class="profile-content">
|
||||
<!-- Personal Information Section -->
|
||||
<section class="profile-section">
|
||||
<h2 class="section-title">@L["Auth_Profile_PersonalInfo"]</h2>
|
||||
|
||||
<EditForm Model="@userProfile" OnValidSubmit="HandleUpdateProfile" FormName="ProfileForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="profile-name">@L["Auth_Register_DisplayName"]</label>
|
||||
<InputText id="profile-name"
|
||||
@bind-Value="userProfile.DisplayName"
|
||||
class="form-input"
|
||||
disabled="@(!isEditingProfile)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="profile-email">@L["Common_Email"]</label>
|
||||
<InputText id="profile-email"
|
||||
@bind-Value="userProfile.Email"
|
||||
class="form-input"
|
||||
disabled />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>@L["Auth_Profile_MemberSince"]</label>
|
||||
<p class="text-secondary">@userProfile.CreatedAt.ToString("MMMM dd, yyyy")</p>
|
||||
</div>
|
||||
|
||||
@if (!isEditingProfile)
|
||||
{
|
||||
<button type="button" class="btn-secondary" @onclick="@(() => isEditingProfile = true)">
|
||||
@L["Auth_Profile_EditProfile"]
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn-primary" disabled="@isSubmitting">
|
||||
@L["Auth_Profile_SaveChanges"]
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" @onclick="CancelEditProfile">
|
||||
@L["Common_Cancel"]
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
<!-- Security Section -->
|
||||
<section class="profile-section">
|
||||
<h2 class="section-title">@L["Auth_Profile_Security"]</h2>
|
||||
|
||||
<EditForm Model="@changePasswordModel" OnValidSubmit="HandleChangePassword" FormName="ChangePasswordForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current-password">@L["Auth_Profile_CurrentPassword"] *</label>
|
||||
<InputText id="current-password"
|
||||
@bind-Value="changePasswordModel.CurrentPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••" />
|
||||
<ValidationMessage For="() => changePasswordModel.CurrentPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password">@L["Auth_Profile_NewPassword"] *</label>
|
||||
<InputText id="new-password"
|
||||
@bind-Value="changePasswordModel.NewPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••" />
|
||||
<small class="form-hint">@L["Auth_Register_PasswordHint"]</small>
|
||||
<ValidationMessage For="() => changePasswordModel.NewPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm-new-password">@L["Auth_Profile_ConfirmPassword"] *</label>
|
||||
<InputText id="confirm-new-password"
|
||||
@bind-Value="changePasswordModel.ConfirmPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••" />
|
||||
<ValidationMessage For="() => changePasswordModel.ConfirmPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner-small"></span>
|
||||
<span>@L["Common_Loading"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_Profile_UpdatePassword"]
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="profile-actions">
|
||||
<button class="btn-secondary" @onclick="HandleLogout">
|
||||
@L["Auth_Profile_Logout"]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private UserProfileDto? userProfile;
|
||||
private ChangePasswordDto changePasswordModel = new();
|
||||
private bool isLoading = true;
|
||||
private bool isEditingProfile = false;
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadUserProfile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Load user profile data.
|
||||
/// VI: Tải dữ liệu hồ sơ người dùng.
|
||||
/// </summary>
|
||||
private async Task LoadUserProfile()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync("api/auth/profile");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
|
||||
userProfile = result?.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
// EN: Redirect to login if unauthorized
|
||||
// VI: Chuyển hướng đến đăng nhập nếu chưa xác thực
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
success = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle profile update.
|
||||
/// VI: Xử lý cập nhật hồ sơ.
|
||||
/// </summary>
|
||||
private async Task HandleUpdateProfile()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PutAsJsonAsync("api/auth/profile", userProfile);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
success = true;
|
||||
message = L["Auth_Profile_Success"];
|
||||
isEditingProfile = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_Profile_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle password change.
|
||||
/// VI: Xử lý đổi mật khẩu.
|
||||
/// </summary>
|
||||
private async Task HandleChangePassword()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/auth/change-password", changePasswordModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
success = true;
|
||||
message = L["Auth_Profile_PasswordSuccess"];
|
||||
changePasswordModel = new(); // Clear form
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_Profile_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Cancel profile editing.
|
||||
/// VI: Hủy chỉnh sửa hồ sơ.
|
||||
/// </summary>
|
||||
private void CancelEditProfile()
|
||||
{
|
||||
isEditingProfile = false;
|
||||
// Reload to reset changes
|
||||
LoadUserProfile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle user logout.
|
||||
/// VI: Xử lý đăng xuất.
|
||||
/// </summary>
|
||||
private async Task HandleLogout()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Http.PostAsync("api/auth/logout", null);
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// EN: Even if logout fails, navigate to login
|
||||
// VI: Ngay cả khi đăng xuất thất bại, chuyển về đăng nhập
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
@page "/register"
|
||||
@using WebClientBase.Shared.DTOs
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IStringLocalizer<Register> L
|
||||
|
||||
@*
|
||||
EN: User registration page.
|
||||
VI: Trang đăng ký người dùng.
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_Register_Title"]</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<section class="auth-card">
|
||||
<h1 class="auth-title">@L["Auth_Register_Title"]</h1>
|
||||
<p class="auth-subtitle">@L["Auth_Register_Subtitle"]</p>
|
||||
|
||||
<EditForm Model="@registerModel" OnValidSubmit="HandleRegister" FormName="RegisterForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-name">@L["Auth_Register_DisplayName"] *</label>
|
||||
<InputText id="reg-name"
|
||||
@bind-Value="registerModel.DisplayName"
|
||||
class="form-input"
|
||||
placeholder="John Doe"
|
||||
autocomplete="name" />
|
||||
<ValidationMessage For="() => registerModel.DisplayName" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-email">@L["Auth_Register_Email"] *</label>
|
||||
<InputText id="reg-email"
|
||||
@bind-Value="registerModel.Email"
|
||||
class="form-input"
|
||||
placeholder="email@example.com"
|
||||
autocomplete="email" />
|
||||
<ValidationMessage For="() => registerModel.Email" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-password">@L["Auth_Register_Password"] *</label>
|
||||
<InputText id="reg-password"
|
||||
@bind-Value="registerModel.Password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••"
|
||||
autocomplete="new-password" />
|
||||
<small class="form-hint">@L["Auth_Register_PasswordHint"]</small>
|
||||
<ValidationMessage For="() => registerModel.Password" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reg-confirm">@L["Auth_Register_ConfirmPassword"] *</label>
|
||||
<InputText id="reg-confirm"
|
||||
@bind-Value="registerModel.ConfirmPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••"
|
||||
autocomplete="new-password" />
|
||||
<ValidationMessage For="() => registerModel.ConfirmPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group mb-6">
|
||||
<InputCheckbox id="accept-terms"
|
||||
@bind-Value="registerModel.AcceptTerms"
|
||||
class="form-checkbox" />
|
||||
<label for="accept-terms" class="checkbox-label">@L["Auth_Register_Terms"]</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner-small"></span>
|
||||
<span>@L["Common_Loading"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_Register_Submit"]
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="auth-footer">
|
||||
<span>@L["Auth_Register_HaveAccount"]</span>
|
||||
<a href="/login" class="link-primary">@L["Auth_Register_LoginLink"]</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private RegisterDto registerModel = new();
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle registration form submission.
|
||||
/// VI: Xử lý submit form đăng ký.
|
||||
/// </summary>
|
||||
private async Task HandleRegister()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/auth/register", registerModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserProfileDto>>();
|
||||
if (result?.Success == true)
|
||||
{
|
||||
success = true;
|
||||
message = L["Auth_Register_Success"];
|
||||
|
||||
// EN: Redirect to login after 2 seconds
|
||||
// VI: Chuyển hướng đến đăng nhập sau 2 giây
|
||||
await Task.Delay(2000);
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = result?.Error ?? L["Auth_Register_Error"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
message = $"{L["Auth_Register_Error"]}: {content}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
@page "/reset-password"
|
||||
@using WebClientBase.Shared.DTOs
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IStringLocalizer<ResetPassword> L
|
||||
|
||||
@*
|
||||
EN: Reset password page with token validation.
|
||||
VI: Trang đặt lại mật khẩu với xác thực token.
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_ResetPassword_Title"]</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<section class="auth-card">
|
||||
<h1 class="auth-title">@L["Auth_ResetPassword_Title"]</h1>
|
||||
<p class="auth-subtitle">@L["Auth_ResetPassword_Subtitle"]</p>
|
||||
|
||||
@if (invalidToken)
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
@L["Auth_ResetPassword_InvalidToken"]
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<a href="/forgot-password" class="link-primary">@L["Auth_ForgotPassword_Title"]</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="@resetPasswordModel" OnValidSubmit="HandleResetPassword" FormName="ResetPasswordForm">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password">@L["Auth_ResetPassword_NewPassword"] *</label>
|
||||
<InputText id="new-password"
|
||||
@bind-Value="resetPasswordModel.NewPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••"
|
||||
autocomplete="new-password" />
|
||||
<small class="form-hint">@L["Auth_ResetPassword_PasswordHint"]</small>
|
||||
<ValidationMessage For="() => resetPasswordModel.NewPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">@L["Auth_ResetPassword_ConfirmPassword"] *</label>
|
||||
<InputText id="confirm-password"
|
||||
@bind-Value="resetPasswordModel.ConfirmPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="••••••••"
|
||||
autocomplete="new-password" />
|
||||
<ValidationMessage For="() => resetPasswordModel.ConfirmPassword" class="validation-message" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary btn-full" disabled="@isSubmitting">
|
||||
@if (isSubmitting)
|
||||
{
|
||||
<span class="spinner-small"></span>
|
||||
<span>@L["Common_Loading"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_ResetPassword_Submit"]
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
|
||||
@if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
<div class="alert @(success ? "alert-success" : "alert-error")">
|
||||
@message
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ResetPasswordDto resetPasswordModel = new();
|
||||
private bool isSubmitting = false;
|
||||
private string message = "";
|
||||
private bool success = false;
|
||||
private bool invalidToken = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// EN: Parse query parameters for token and email
|
||||
// VI: Phân tích query parameters cho token và email
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
var query = uri.Query;
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
var queryParams = System.Web.HttpUtility.ParseQueryString(query);
|
||||
var token = queryParams["token"];
|
||||
var email = queryParams["email"];
|
||||
|
||||
if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(email))
|
||||
{
|
||||
resetPasswordModel.Token = token;
|
||||
resetPasswordModel.Email = email;
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidToken = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handle reset password form submission.
|
||||
/// VI: Xử lý submit form đặt lại mật khẩu.
|
||||
/// </summary>
|
||||
private async Task HandleResetPassword()
|
||||
{
|
||||
isSubmitting = true;
|
||||
message = "";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/auth/reset-password", resetPasswordModel);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||
if (result?.Success == true)
|
||||
{
|
||||
success = true;
|
||||
message = L["Auth_ResetPassword_Success"];
|
||||
|
||||
// EN: Redirect to login after 2 seconds
|
||||
// VI: Chuyển hướng đến đăng nhập sau 2 giây
|
||||
await Task.Delay(2000);
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = result?.Error ?? L["Auth_ResetPassword_Error"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_ResetPassword_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
@page "/verify-email"
|
||||
@using WebClientBase.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IStringLocalizer<VerifyEmail> L
|
||||
|
||||
@*
|
||||
EN: Email verification page.
|
||||
VI: Trang xác minh email.
|
||||
*@
|
||||
|
||||
<PageTitle>@L["Auth_VerifyEmail_Title"]</PageTitle>
|
||||
|
||||
<div class="auth-container">
|
||||
<section class="auth-card text-center">
|
||||
<h1 class="auth-title">@L["Auth_VerifyEmail_Title"]</h1>
|
||||
|
||||
@if (isVerifying)
|
||||
{
|
||||
<div class="loading-state">
|
||||
<span class="spinner"></span>
|
||||
<p class="mt-4">@L["Auth_VerifyEmail_Verifying"]</p>
|
||||
</div>
|
||||
}
|
||||
else if (success)
|
||||
{
|
||||
<div class="success-state">
|
||||
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 6L9 17l-5-5"/>
|
||||
</svg>
|
||||
<p class="mt-4 text-lg">@L["Auth_VerifyEmail_Success"]</p>
|
||||
<button class="btn-primary mt-6" @onclick="@(() => Navigation.NavigateTo("/login"))">
|
||||
@L["Auth_VerifyEmail_GoToLogin"]
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
@message
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<a href="/login" class="link-primary">@L["Auth_VerifyEmail_GoToLogin"]</a>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool isVerifying = true;
|
||||
private bool success = false;
|
||||
private string message = "";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await VerifyEmailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Verify email using token from query parameters.
|
||||
/// VI: Xác minh email sử dụng token từ query parameters.
|
||||
/// </summary>
|
||||
private async Task VerifyEmailAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// EN: Parse query parameters
|
||||
// VI: Phân tích query parameters
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
var query = uri.Query;
|
||||
|
||||
string? token = null;
|
||||
string? userId = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
var queryParams = System.Web.HttpUtility.ParseQueryString(query);
|
||||
token = queryParams["token"];
|
||||
userId = queryParams["userId"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId))
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_VerifyEmail_InvalidToken"];
|
||||
isVerifying = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// EN: Call verification endpoint
|
||||
// VI: Gọi endpoint xác minh
|
||||
var response = await Http.PostAsJsonAsync("api/auth/verify-email", new {
|
||||
Token = token.ToString(),
|
||||
UserId = userId.ToString()
|
||||
});
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||
success = result?.Success == true;
|
||||
message = success ? L["Auth_VerifyEmail_Success"] : (result?.Error ?? L["Auth_VerifyEmail_Error"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
message = L["Auth_VerifyEmail_Error"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
success = false;
|
||||
message = $"{L["Common_Error"]}: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isVerifying = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,4 +365,386 @@ h6 {
|
||||
color: var(--primitive-neutral-50) !important;
|
||||
/* Force White */
|
||||
border-color: var(--primitive-neutral-700) !important;
|
||||
}
|
||||
|
||||
/* ═════════════════════════════════════════════════════════════════════════
|
||||
6. AUTHENTICATION COMPONENTS
|
||||
═════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Auth Container */
|
||||
.auth-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 64px);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
/* Auth Card */
|
||||
.auth-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--space-12);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: var(--space-6);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Form Components */
|
||||
.form-group {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--action-primary-bg);
|
||||
box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(250, 250, 250, 0.1);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background-color: var(--bg-surface-hover);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: var(--space-1);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
margin-bottom: 0;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.form-actions-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
border-radius: var(--border-radius-base);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--action-primary-bg);
|
||||
color: var(--action-primary-text);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--action-primary-bg-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--action-secondary-bg);
|
||||
color: var(--action-secondary-text);
|
||||
border: 1px solid var(--action-secondary-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: var(--action-secondary-bg-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled,
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.link-primary {
|
||||
color: var(--action-primary-bg);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.link-primary:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-secondary {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.link-secondary:hover {
|
||||
color: var(--text-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Validation */
|
||||
.validation-message {
|
||||
display: block;
|
||||
margin-top: var(--space-1);
|
||||
font-size: 0.875rem;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--border-radius-base);
|
||||
margin-top: var(--space-6);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
border: 1px solid #6ee7b7;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-success {
|
||||
background-color: #064e3b;
|
||||
color: #6ee7b7;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-error {
|
||||
background-color: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
/* Spinners */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--border-subtle);
|
||||
border-top-color: var(--action-primary-bg);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-small {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-12);
|
||||
}
|
||||
|
||||
/* Success State (for verification pages) */
|
||||
.success-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
color: #10b981;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Profile Page Specific */
|
||||
.profile-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-8) var(--space-6);
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
margin-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.profile-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.profile-section {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-6);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
margin-top: var(--space-8);
|
||||
padding-top: var(--space-8);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: var(--space-6);
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: var(--space-2);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -19,5 +19,79 @@
|
||||
"Solution_POS_Title": "Multi-Industry POS",
|
||||
"Solution_POS_Desc": "Unified commerce for retail and services. Manage inventory, sales, and staff from a single dashboard.",
|
||||
"LearnMore": "Learn more",
|
||||
"FooterCopyright": "© 2024 GoodGo Enterprise. All rights reserved."
|
||||
"FooterCopyright": "© 2024 GoodGo Enterprise. All rights reserved.",
|
||||
"Auth_Login_Title": "Welcome Back",
|
||||
"Auth_Login_Subtitle": "Sign in to your account",
|
||||
"Auth_Login_Email": "Email",
|
||||
"Auth_Login_Password": "Password",
|
||||
"Auth_Login_RememberMe": "Remember me",
|
||||
"Auth_Login_ForgotPassword": "Forgot password?",
|
||||
"Auth_Login_Submit": "Sign In",
|
||||
"Auth_Login_NoAccount": "Don't have an account?",
|
||||
"Auth_Login_RegisterLink": "Register here",
|
||||
"Auth_Login_Success": "Welcome back, {0}!",
|
||||
"Auth_Login_Error": "Login failed. Please check your credentials.",
|
||||
"Auth_Register_Title": "Create Account",
|
||||
"Auth_Register_Subtitle": "Join us today",
|
||||
"Auth_Register_DisplayName": "Display Name",
|
||||
"Auth_Register_Email": "Email",
|
||||
"Auth_Register_Password": "Password",
|
||||
"Auth_Register_ConfirmPassword": "Confirm Password",
|
||||
"Auth_Register_PasswordHint": "Min 8 chars, uppercase, lowercase, digit",
|
||||
"Auth_Register_Terms": "I agree to the Terms of Service and Privacy Policy",
|
||||
"Auth_Register_Submit": "Create Account",
|
||||
"Auth_Register_HaveAccount": "Already have an account?",
|
||||
"Auth_Register_LoginLink": "Login here",
|
||||
"Auth_Register_Success": "Account created successfully! Please check your email to verify your account.",
|
||||
"Auth_Register_Error": "Registration failed. Please try again.",
|
||||
"Auth_ForgotPassword_Title": "Forgot Password",
|
||||
"Auth_ForgotPassword_Subtitle": "Enter your email to receive a password reset link",
|
||||
"Auth_ForgotPassword_Email": "Email",
|
||||
"Auth_ForgotPassword_Submit": "Send Reset Link",
|
||||
"Auth_ForgotPassword_BackToLogin": "Back to Login",
|
||||
"Auth_ForgotPassword_Success": "Password reset link sent! Please check your email.",
|
||||
"Auth_ForgotPassword_Error": "Failed to send reset link. Please try again.",
|
||||
"Auth_ResetPassword_Title": "Reset Password",
|
||||
"Auth_ResetPassword_Subtitle": "Enter your new password",
|
||||
"Auth_ResetPassword_NewPassword": "New Password",
|
||||
"Auth_ResetPassword_ConfirmPassword": "Confirm Password",
|
||||
"Auth_ResetPassword_PasswordHint": "Min 8 chars, uppercase, lowercase, digit",
|
||||
"Auth_ResetPassword_Submit": "Reset Password",
|
||||
"Auth_ResetPassword_Success": "Password reset successfully! You can now login.",
|
||||
"Auth_ResetPassword_Error": "Failed to reset password. The link may have expired.",
|
||||
"Auth_ResetPassword_InvalidToken": "Invalid or expired reset token.",
|
||||
"Auth_VerifyEmail_Title": "Email Verification",
|
||||
"Auth_VerifyEmail_Verifying": "Verifying your email...",
|
||||
"Auth_VerifyEmail_Success": "Email verified successfully! You can now login.",
|
||||
"Auth_VerifyEmail_Error": "Email verification failed. The link may have expired.",
|
||||
"Auth_VerifyEmail_InvalidToken": "Invalid or expired verification token.",
|
||||
"Auth_VerifyEmail_GoToLogin": "Go to Login",
|
||||
"Auth_Profile_Title": "My Profile",
|
||||
"Auth_Profile_Subtitle": "Manage your account information",
|
||||
"Auth_Profile_PersonalInfo": "Personal Information",
|
||||
"Auth_Profile_Security": "Security",
|
||||
"Auth_Profile_MemberSince": "Member since",
|
||||
"Auth_Profile_EditProfile": "Edit Profile",
|
||||
"Auth_Profile_SaveChanges": "Save Changes",
|
||||
"Auth_Profile_ChangePassword": "Change Password",
|
||||
"Auth_Profile_CurrentPassword": "Current Password",
|
||||
"Auth_Profile_NewPassword": "New Password",
|
||||
"Auth_Profile_ConfirmPassword": "Confirm New Password",
|
||||
"Auth_Profile_UpdatePassword": "Update Password",
|
||||
"Auth_Profile_Logout": "Logout",
|
||||
"Auth_Profile_Success": "Profile updated successfully!",
|
||||
"Auth_Profile_PasswordSuccess": "Password changed successfully!",
|
||||
"Auth_Profile_Error": "Failed to update profile. Please try again.",
|
||||
"Validation_Required": "This field is required",
|
||||
"Validation_EmailInvalid": "Please enter a valid email address",
|
||||
"Validation_PasswordTooShort": "Password must be at least 8 characters",
|
||||
"Validation_PasswordMismatch": "Passwords do not match",
|
||||
"Validation_PasswordRequirements": "Password must contain uppercase, lowercase, number, and special character",
|
||||
"Validation_DisplayNameTooShort": "Display name must be at least 2 characters",
|
||||
"Validation_TermsRequired": "You must accept the Terms of Service",
|
||||
"Common_Email": "Email",
|
||||
"Common_Password": "Password",
|
||||
"Common_Loading": "Loading...",
|
||||
"Common_Success": "Success",
|
||||
"Common_Error": "Error"
|
||||
}
|
||||
@@ -19,5 +19,79 @@
|
||||
"Solution_POS_Title": "POS Đa Ngành",
|
||||
"Solution_POS_Desc": "Thương mại hợp nhất cho bán lẻ và dịch vụ. Quản lý tồn kho, bán hàng và nhân viên từ một bảng điều khiển.",
|
||||
"LearnMore": "Tìm hiểu thêm",
|
||||
"FooterCopyright": "© 2024 GoodGo Enterprise. Bảo lưu mọi quyền."
|
||||
"FooterCopyright": "© 2024 GoodGo Enterprise. Bảo lưu mọi quyền.",
|
||||
"Auth_Login_Title": "Chào Mừng Trở Lại",
|
||||
"Auth_Login_Subtitle": "Đăng nhập vào tài khoản của bạn",
|
||||
"Auth_Login_Email": "Email",
|
||||
"Auth_Login_Password": "Mật khẩu",
|
||||
"Auth_Login_RememberMe": "Ghi nhớ đăng nhập",
|
||||
"Auth_Login_ForgotPassword": "Quên mật khẩu?",
|
||||
"Auth_Login_Submit": "Đăng nhập",
|
||||
"Auth_Login_NoAccount": "Chưa có tài khoản?",
|
||||
"Auth_Login_RegisterLink": "Đăng ký tại đây",
|
||||
"Auth_Login_Success": "Chào mừng trở lại, {0}!",
|
||||
"Auth_Login_Error": "Đăng nhập thất bại. Vui lòng kiểm tra thông tin đăng nhập.",
|
||||
"Auth_Register_Title": "Tạo Tài Khoản",
|
||||
"Auth_Register_Subtitle": "Tham gia với chúng tôi ngay hôm nay",
|
||||
"Auth_Register_DisplayName": "Tên hiển thị",
|
||||
"Auth_Register_Email": "Email",
|
||||
"Auth_Register_Password": "Mật khẩu",
|
||||
"Auth_Register_ConfirmPassword": "Xác nhận mật khẩu",
|
||||
"Auth_Register_PasswordHint": "Tối thiểu 8 ký tự, chữ hoa, chữ thường, số",
|
||||
"Auth_Register_Terms": "Tôi đồng ý với Điều khoản Dịch vụ và Chính sách Bảo mật",
|
||||
"Auth_Register_Submit": "Tạo tài khoản",
|
||||
"Auth_Register_HaveAccount": "Đã có tài khoản?",
|
||||
"Auth_Register_LoginLink": "Đăng nhập tại đây",
|
||||
"Auth_Register_Success": "Tạo tài khoản thành công! Vui lòng kiểm tra email để xác minh tài khoản.",
|
||||
"Auth_Register_Error": "Đăng ký thất bại. Vui lòng thử lại.",
|
||||
"Auth_ForgotPassword_Title": "Quên Mật Khẩu",
|
||||
"Auth_ForgotPassword_Subtitle": "Nhập email của bạn để nhận liên kết đặt lại mật khẩu",
|
||||
"Auth_ForgotPassword_Email": "Email",
|
||||
"Auth_ForgotPassword_Submit": "Gửi Liên Kết Đặt Lại",
|
||||
"Auth_ForgotPassword_BackToLogin": "Quay lại Đăng nhập",
|
||||
"Auth_ForgotPassword_Success": "Liên kết đặt lại mật khẩu đã được gửi! Vui lòng kiểm tra email.",
|
||||
"Auth_ForgotPassword_Error": "Không thể gửi liên kết đặt lại. Vui lòng thử lại.",
|
||||
"Auth_ResetPassword_Title": "Đặt Lại Mật Khẩu",
|
||||
"Auth_ResetPassword_Subtitle": "Nhập mật khẩu mới của bạn",
|
||||
"Auth_ResetPassword_NewPassword": "Mật khẩu mới",
|
||||
"Auth_ResetPassword_ConfirmPassword": "Xác nhận mật khẩu",
|
||||
"Auth_ResetPassword_PasswordHint": "Tối thiểu 8 ký tự, chữ hoa, chữ thường, số",
|
||||
"Auth_ResetPassword_Submit": "Đặt Lại Mật Khẩu",
|
||||
"Auth_ResetPassword_Success": "Đặt lại mật khẩu thành công! Bây giờ bạn có thể đăng nhập.",
|
||||
"Auth_ResetPassword_Error": "Không thể đặt lại mật khẩu. Liên kết có thể đã hết hạn.",
|
||||
"Auth_ResetPassword_InvalidToken": "Mã xác thực không hợp lệ hoặc đã hết hạn.",
|
||||
"Auth_VerifyEmail_Title": "Xác Minh Email",
|
||||
"Auth_VerifyEmail_Verifying": "Đang xác minh email của bạn...",
|
||||
"Auth_VerifyEmail_Success": "Xác minh email thành công! Bây giờ bạn có thể đăng nhập.",
|
||||
"Auth_VerifyEmail_Error": "Xác minh email thất bại. Liên kết có thể đã hết hạn.",
|
||||
"Auth_VerifyEmail_InvalidToken": "Mã xác thực không hợp lệ hoặc đã hết hạn.",
|
||||
"Auth_VerifyEmail_GoToLogin": "Đi đến Đăng nhập",
|
||||
"Auth_Profile_Title": "Hồ Sơ Của Tôi",
|
||||
"Auth_Profile_Subtitle": "Quản lý thông tin tài khoản của bạn",
|
||||
"Auth_Profile_PersonalInfo": "Thông Tin Cá Nhân",
|
||||
"Auth_Profile_Security": "Bảo Mật",
|
||||
"Auth_Profile_MemberSince": "Thành viên từ",
|
||||
"Auth_Profile_EditProfile": "Chỉnh Sửa Hồ Sơ",
|
||||
"Auth_Profile_SaveChanges": "Lưu Thay Đổi",
|
||||
"Auth_Profile_ChangePassword": "Đổi Mật Khẩu",
|
||||
"Auth_Profile_CurrentPassword": "Mật khẩu hiện tại",
|
||||
"Auth_Profile_NewPassword": "Mật khẩu mới",
|
||||
"Auth_Profile_ConfirmPassword": "Xác nhận mật khẩu mới",
|
||||
"Auth_Profile_UpdatePassword": "Cập Nhật Mật Khẩu",
|
||||
"Auth_Profile_Logout": "Đăng xuất",
|
||||
"Auth_Profile_Success": "Cập nhật hồ sơ thành công!",
|
||||
"Auth_Profile_PasswordSuccess": "Đổi mật khẩu thành công!",
|
||||
"Auth_Profile_Error": "Không thể cập nhật hồ sơ. Vui lòng thử lại.",
|
||||
"Validation_Required": "Trường này là bắt buộc",
|
||||
"Validation_EmailInvalid": "Vui lòng nhập địa chỉ email hợp lệ",
|
||||
"Validation_PasswordTooShort": "Mật khẩu phải có ít nhất 8 ký tự",
|
||||
"Validation_PasswordMismatch": "Mật khẩu không khớp",
|
||||
"Validation_PasswordRequirements": "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt",
|
||||
"Validation_DisplayNameTooShort": "Tên hiển thị phải có ít nhất 2 ký tự",
|
||||
"Validation_TermsRequired": "Bạn phải chấp nhận Điều khoản Dịch vụ",
|
||||
"Common_Email": "Email",
|
||||
"Common_Password": "Mật khẩu",
|
||||
"Common_Loading": "Đang tải...",
|
||||
"Common_Success": "Thành công",
|
||||
"Common_Error": "Lỗi"
|
||||
}
|
||||
Reference in New Issue
Block a user