feat: Set up initial WebClientTpos .NET project structure, including client, server, assets, and documentation.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user