feat: Set up initial WebClientTpos .NET project structure, including client, server, assets, and documentation.

This commit is contained in:
Ho Ngoc Hai
2026-02-12 00:41:43 +07:00
parent b661bb7d8b
commit 689f4fa96f
51 changed files with 4860 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
@page "/forgot-password"
@using WebClientTpos.Shared.DTOs
@using WebClientTpos.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;
}
}
}

View File

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

View File

@@ -0,0 +1,295 @@
@page "/profile"
@using WebClientTpos.Shared.DTOs
@using WebClientTpos.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 async Task CancelEditProfile()
{
isEditingProfile = false;
// Reload to reset changes
await 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");
}
}
}

View File

@@ -0,0 +1,155 @@
@page "/register"
@using WebClientTpos.Shared.DTOs
@using WebClientTpos.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;
}
}
}

View File

@@ -0,0 +1,164 @@
@page "/reset-password"
@using WebClientTpos.Shared.DTOs
@using WebClientTpos.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;
}
}
}

View File

@@ -0,0 +1,124 @@
@page "/verify-email"
@using WebClientTpos.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();
}
}
}

View File

@@ -0,0 +1,302 @@
@page "/"
@inject IStringLocalizer<Home> L
<PageTitle>aPOS - @L["HeroHeadline"]</PageTitle>
<!-- ═══════════════════════════════════════════════════════════════════════
1. HERO SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section class="hero-section">
<div class="hero-badge">@((MarkupString)L["HeroBadge"].Value)</div>
<h1 class="hero-headline">
@((MarkupString)L["HeroHeadline"].Value)
</h1>
<p class="hero-subtext">
@L["HeroSubtext"]
</p>
<div class="hero-actions">
<a href="#pricing" class="btn-accent btn-accent-lg">@L["HeroCTA_Primary"]</a>
<a href="#features" class="btn-outline btn-outline-lg">@L["HeroCTA_Secondary"]</a>
</div>
<div class="hero-mockup">
<span>@L["HeroMockup_Alt"]</span>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
2. TRUST SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section class="trust-section">
<div class="container">
<p class="trust-label">@L["Trust_Label"]</p>
<div class="trust-stats">
<span class="trust-stat">@((MarkupString)L["Trust_Stat1"].Value)</span>
<span class="trust-stat">@((MarkupString)L["Trust_Stat2"].Value)</span>
<span class="trust-stat">@((MarkupString)L["Trust_Stat3"].Value)</span>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
3. FEATURES SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section id="features" class="tpos-section">
<div class="container">
<div class="tpos-section-header">
<div class="tpos-badge">@L["Features_Badge"]</div>
<h2 class="tpos-section-title">@L["Features_Title"]</h2>
<p class="tpos-section-desc">@L["Features_Desc"]</p>
</div>
<div class="tpos-feature-grid">
@foreach (var f in _features)
{
<div class="tpos-feature-card">
<div class="tpos-feature-icon">@((MarkupString)L[f.Icon].Value)</div>
<h3 class="tpos-feature-title">@L[f.Title]</h3>
<p class="tpos-feature-desc">@L[f.Desc]</p>
</div>
}
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
4. INDUSTRIES SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section id="industries" class="tpos-section" style="background: var(--bg-surface);">
<div class="container">
<div class="tpos-section-header">
<div class="tpos-badge">@L["Industries_Badge"]</div>
<h2 class="tpos-section-title">@L["Industries_Title"]</h2>
<p class="tpos-section-desc">@L["Industries_Desc"]</p>
</div>
<div class="tpos-industry-grid">
@foreach (var ind in _industries)
{
<div class="tpos-industry-card">
<h3 class="tpos-industry-title">@L[ind.Title]</h3>
<p class="tpos-industry-desc">@L[ind.Desc]</p>
<div class="tpos-chips">
@foreach (var chip in L[ind.Chips].Value.Split(','))
{
<span class="tpos-chip">✨ @chip.Trim()</span>
}
</div>
</div>
}
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
5. ONBOARDING STEPS
═══════════════════════════════════════════════════════════════════════ -->
<section class="tpos-section">
<div class="container">
<div class="tpos-section-header">
<div class="tpos-badge">@L["Steps_Badge"]</div>
<h2 class="tpos-section-title">@L["Steps_Title"]</h2>
<p class="tpos-section-desc">@L["Steps_Desc"]</p>
</div>
<div class="tpos-steps">
<div class="tpos-step">
<div class="tpos-step-num">1</div>
<h3 class="tpos-step-title">@L["Step1_Title"]</h3>
<p class="tpos-step-desc">@L["Step1_Desc"]</p>
</div>
<div class="tpos-step">
<div class="tpos-step-num">2</div>
<h3 class="tpos-step-title">@L["Step2_Title"]</h3>
<p class="tpos-step-desc">@L["Step2_Desc"]</p>
</div>
<div class="tpos-step">
<div class="tpos-step-num">3</div>
<h3 class="tpos-step-title">@L["Step3_Title"]</h3>
<p class="tpos-step-desc">@L["Step3_Desc"]</p>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
6. PRICING SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section id="pricing" class="tpos-section" style="background: var(--bg-surface);">
<div class="container">
<div class="tpos-section-header">
<div class="tpos-badge">@L["Pricing_Badge"]</div>
<h2 class="tpos-section-title">@L["Pricing_Title"]</h2>
<p class="tpos-section-desc">@L["Pricing_Desc"]</p>
</div>
<div class="tpos-pricing-grid">
<!-- Starter Plan -->
<div class="tpos-pricing-card">
<div class="tpos-pricing-badge">@L["Plan_Starter_Badge"]</div>
<div class="tpos-pricing-name">@L["Plan_Starter_Name"]</div>
<div class="tpos-pricing-price">
<span class="tpos-pricing-amount">@L["Plan_Starter_Price"]</span>
<span class="tpos-pricing-period">@L["Plan_Starter_Period"]</span>
</div>
<p class="tpos-pricing-desc">@L["Plan_Starter_Desc"]</p>
<ul class="tpos-pricing-features">
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Starter_Feature1"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Starter_Feature2"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Starter_Feature3"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Starter_Feature4"]</li>
</ul>
<a href="#" class="btn-outline btn-full">@L["Plan_Starter_CTA"]</a>
</div>
<!-- Professional Plan (Featured) -->
<div class="tpos-pricing-card featured">
<div class="tpos-pricing-badge">@L["Plan_Pro_Badge"]</div>
<div class="tpos-pricing-name">@L["Plan_Pro_Name"]</div>
<div class="tpos-pricing-price">
<span class="tpos-pricing-amount">@L["Plan_Pro_Price"]</span>
<span class="tpos-pricing-period">@L["Plan_Pro_Period"]</span>
</div>
<p class="tpos-pricing-desc">@L["Plan_Pro_Desc"]</p>
<ul class="tpos-pricing-features">
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Pro_Feature1"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Pro_Feature2"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Pro_Feature3"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Pro_Feature4"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Pro_Feature5"]</li>
</ul>
<a href="#" class="btn-accent btn-full">@L["Plan_Pro_CTA"]</a>
</div>
<!-- Enterprise Plan -->
<div class="tpos-pricing-card">
<div class="tpos-pricing-badge">@L["Plan_Enterprise_Badge"]</div>
<div class="tpos-pricing-name">@L["Plan_Enterprise_Name"]</div>
<div class="tpos-pricing-price">
<span class="tpos-pricing-amount">@L["Plan_Enterprise_Price"]</span>
<span class="tpos-pricing-period">@L["Plan_Enterprise_Period"]</span>
</div>
<p class="tpos-pricing-desc">@L["Plan_Enterprise_Desc"]</p>
<ul class="tpos-pricing-features">
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Enterprise_Feature1"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Enterprise_Feature2"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Enterprise_Feature3"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Enterprise_Feature4"]</li>
<li class="tpos-pricing-feature"><span class="check-icon">✓</span> @L["Plan_Enterprise_Feature5"]</li>
</ul>
<a href="#" class="btn-outline btn-full">@L["Plan_Enterprise_CTA"]</a>
</div>
</div>
<!-- Add-ons -->
<div class="tpos-addons">
<h3 class="tpos-addons-title">@L["Addons_Title"]</h3>
<div class="tpos-addons-grid">
@foreach (var addon in _addons)
{
<div class="tpos-addon-item">
<div class="tpos-addon-name">@L[addon.Name]</div>
<div class="tpos-addon-price">@L[addon.Price]</div>
</div>
}
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
7. CTA SECTION
═══════════════════════════════════════════════════════════════════════ -->
<section class="tpos-cta">
<h2 class="tpos-cta-title">@L["CTA_Title"]</h2>
<p class="tpos-cta-sub">@L["CTA_Subtitle"]</p>
<div class="tpos-cta-actions">
<a href="#" class="btn-accent btn-accent-lg">@L["CTA_Primary"]</a>
<a href="#" class="btn-outline btn-outline-lg">@L["CTA_Secondary"]</a>
</div>
<p class="tpos-cta-trust">@L["CTA_Trust"]</p>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════
8. FOOTER
═══════════════════════════════════════════════════════════════════════ -->
<footer class="tpos-footer">
<div class="tpos-footer-grid">
<div class="tpos-footer-brand">
<span class="tpos-logo">@L["AppName"]</span>
<p class="tpos-footer-tagline">@L["Footer_Tagline"]</p>
</div>
<div>
<div class="tpos-footer-col-title">@L["Footer_Col1_Title"]</div>
<ul class="tpos-footer-links">
<li><a href="#features" class="tpos-footer-link">@L["Footer_Col1_Link1"]</a></li>
<li><a href="#pricing" class="tpos-footer-link">@L["Footer_Col1_Link2"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col1_Link3"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col1_Link4"]</a></li>
</ul>
</div>
<div>
<div class="tpos-footer-col-title">@L["Footer_Col2_Title"]</div>
<ul class="tpos-footer-links">
<li><a href="#" class="tpos-footer-link">@L["Footer_Col2_Link1"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col2_Link2"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col2_Link3"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col2_Link4"]</a></li>
</ul>
</div>
<div>
<div class="tpos-footer-col-title">@L["Footer_Col3_Title"]</div>
<ul class="tpos-footer-links">
<li><a href="#" class="tpos-footer-link">@L["Footer_Col3_Link1"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col3_Link2"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col3_Link3"]</a></li>
<li><a href="#" class="tpos-footer-link">@L["Footer_Col3_Link4"]</a></li>
</ul>
</div>
</div>
<div class="tpos-footer-copy">@L["Footer_Copyright"]</div>
</footer>
@code {
// Feature cards data
private record FeatureItem(string Icon, string Title, string Desc);
private readonly FeatureItem[] _features =
[
new("Feature_POS_Icon", "Feature_POS_Title", "Feature_POS_Desc"),
new("Feature_Loyalty_Icon", "Feature_Loyalty_Title", "Feature_Loyalty_Desc"),
new("Feature_Reports_Icon", "Feature_Reports_Title", "Feature_Reports_Desc"),
new("Feature_Staff_Icon", "Feature_Staff_Title", "Feature_Staff_Desc"),
new("Feature_Inventory_Icon", "Feature_Inventory_Title", "Feature_Inventory_Desc"),
new("Feature_Payments_Icon", "Feature_Payments_Title", "Feature_Payments_Desc"),
];
// Industry cards data
private record IndustryItem(string Title, string Desc, string Chips);
private readonly IndustryItem[] _industries =
[
new("Industry_Restaurant_Title", "Industry_Restaurant_Desc", "Industry_Restaurant_Chips"),
new("Industry_Bar_Title", "Industry_Bar_Desc", "Industry_Bar_Chips"),
new("Industry_Karaoke_Title", "Industry_Karaoke_Desc", "Industry_Karaoke_Chips"),
new("Industry_Coffee_Title", "Industry_Coffee_Desc", "Industry_Coffee_Chips"),
new("Industry_Spa_Title", "Industry_Spa_Desc", "Industry_Spa_Chips"),
new("Industry_Retail_Title", "Industry_Retail_Desc", "Industry_Retail_Chips"),
];
// Add-on modules data
private record AddonItem(string Name, string Price);
private readonly AddonItem[] _addons =
[
new("Addon_KDS_Name", "Addon_KDS_Price"),
new("Addon_Delivery_Name", "Addon_Delivery_Price"),
new("Addon_Accounting_Name", "Addon_Accounting_Price"),
new("Addon_EInvoice_Name", "Addon_EInvoice_Price"),
new("Addon_Marketing_Name", "Addon_Marketing_Price"),
new("Addon_Reservation_Name", "Addon_Reservation_Price"),
];
}

View File

@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>