feat: Restructure authentication UI to dedicated tPOS pages and update client assets and configurations.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@using System.Globalization
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<MudMenu Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight" LockScroll="true">
|
||||
<ActivatorContent>
|
||||
@@ -29,39 +30,37 @@
|
||||
@code {
|
||||
private string GetCurrentLabel()
|
||||
{
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
var path = uri.PathAndQuery;
|
||||
|
||||
// Simple heuristic: if path starts with /vi-VN or /vi, show VI. Default EN.
|
||||
if (path.StartsWith("/vi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "VI";
|
||||
}
|
||||
return "EN";
|
||||
var culture = CultureInfo.CurrentUICulture.Name;
|
||||
return culture.StartsWith("vi", StringComparison.OrdinalIgnoreCase) ? "VI" : "EN";
|
||||
}
|
||||
|
||||
private void SwitchLanguage(string targetCulture)
|
||||
private async Task SwitchLanguage(string targetCulture)
|
||||
{
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
var path = uri.PathAndQuery;
|
||||
// Save to localStorage for persistence across page reloads
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "aPOS_culture", targetCulture);
|
||||
|
||||
// Force full page reload to reinitialize the WASM app with new culture
|
||||
var currentUri = Navigation.Uri;
|
||||
var uri = new Uri(currentUri);
|
||||
|
||||
// Build clean URL (strip any culture segments from path)
|
||||
var path = uri.AbsolutePath;
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
string newPath;
|
||||
if (segments.Length > 0 && (segments[0].Equals("vi-VN", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("en-US", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("vi", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("en", StringComparison.OrdinalIgnoreCase)))
|
||||
// Remove existing culture segment if present
|
||||
if (segments.Length > 0 && (
|
||||
segments[0].Equals("vi-VN", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("en-US", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("vi", StringComparison.OrdinalIgnoreCase) ||
|
||||
segments[0].Equals("en", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
segments[0] = targetCulture;
|
||||
newPath = "/" + string.Join('/', segments);
|
||||
segments = segments.Skip(1).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (path == "/") path = "";
|
||||
newPath = $"/{targetCulture}{path}";
|
||||
}
|
||||
|
||||
Navigation.NavigateTo(newPath, forceLoad: true);
|
||||
|
||||
var cleanPath = "/" + string.Join('/', segments);
|
||||
if (cleanPath == "/") cleanPath = "";
|
||||
|
||||
// Navigate to root with forceLoad to reinitialize WASM
|
||||
Navigation.NavigateTo($"{cleanPath}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ public class LocalizationCache
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private Dictionary<string, string> _strings = new();
|
||||
private bool _isLoaded;
|
||||
private string _loadedCulture = "";
|
||||
|
||||
public LocalizationCache(HttpClient httpClient)
|
||||
{
|
||||
@@ -25,21 +25,22 @@ public class LocalizationCache
|
||||
|
||||
public async Task LoadAsync(CultureInfo culture)
|
||||
{
|
||||
if (_isLoaded) return; // Or check if culture changed
|
||||
var cultureName = culture.Name;
|
||||
|
||||
// Fallback for simple culture codes
|
||||
if (cultureName == "vi") cultureName = "vi-VN";
|
||||
if (cultureName == "en") cultureName = "en-US";
|
||||
|
||||
// Skip reload only if same culture is already loaded
|
||||
if (_loadedCulture == cultureName) return;
|
||||
|
||||
try
|
||||
{
|
||||
var cultureName = culture.Name;
|
||||
// Map generic "vi" to "vi-VN" if needed, but for now we trust the culture name matches file
|
||||
// Fallback for simple "vi" -> "vi-VN"
|
||||
if (cultureName == "vi") cultureName = "vi-VN";
|
||||
if (cultureName == "en") cultureName = "en-US";
|
||||
|
||||
var loaded = await _httpClient.GetFromJsonAsync<Dictionary<string, string>>($"/locales/{cultureName}.json?v={DateTime.Now.Ticks}");
|
||||
if (loaded != null)
|
||||
{
|
||||
_strings = loaded;
|
||||
_isLoaded = true;
|
||||
_loadedCulture = cultureName;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@page "/{culture}"
|
||||
@inject IStringLocalizer<Home> L
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="hero-mockup">
|
||||
<span>@L["HeroMockup_Alt"]</span>
|
||||
<img src="/images/home/pos-dashboard.png" alt="@L["HeroMockup_Alt"]" class="hero-mockup-img" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -93,13 +94,18 @@
|
||||
@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 class="tpos-industry-img">
|
||||
<img src="@ind.Image" alt="@L[ind.Title]" />
|
||||
</div>
|
||||
<div class="tpos-industry-content">
|
||||
<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>
|
||||
}
|
||||
@@ -159,9 +165,9 @@
|
||||
<p class="tpos-pricing-desc">@L["Plan_Starter_Desc"]</p>
|
||||
<hr class="tpos-pricing-divider" />
|
||||
<ul class="tpos-pricing-features">
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Starter_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Starter_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Starter_Feature3"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Starter_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Starter_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Starter_Feature3"]</li>
|
||||
</ul>
|
||||
<a href="#" class="btn-outline btn-full">@L["Plan_Starter_CTA"]</a>
|
||||
</div>
|
||||
@@ -177,9 +183,9 @@
|
||||
<p class="tpos-pricing-desc">@L["Plan_Pro_Desc"]</p>
|
||||
<hr class="tpos-pricing-divider" />
|
||||
<ul class="tpos-pricing-features">
|
||||
<li class="tpos-pricing-feature"><span class="check-icon accent">✓</span> @L["Plan_Pro_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon accent">✓</span> @L["Plan_Pro_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon accent">✓</span> @L["Plan_Pro_Feature3"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check accent"></i> @L["Plan_Pro_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check accent"></i> @L["Plan_Pro_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check accent"></i> @L["Plan_Pro_Feature3"]</li>
|
||||
</ul>
|
||||
<a href="#" class="btn-accent btn-full">@L["Plan_Pro_CTA"]</a>
|
||||
</div>
|
||||
@@ -194,9 +200,9 @@
|
||||
<p class="tpos-pricing-desc">@L["Plan_Enterprise_Desc"]</p>
|
||||
<hr class="tpos-pricing-divider" />
|
||||
<ul class="tpos-pricing-features">
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Enterprise_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Enterprise_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><span class="check-icon green">✓</span> @L["Plan_Enterprise_Feature3"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Enterprise_Feature1"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Enterprise_Feature2"]</li>
|
||||
<li class="tpos-pricing-feature"><i data-lucide="check" class="lucide-check green"></i> @L["Plan_Enterprise_Feature3"]</li>
|
||||
</ul>
|
||||
<a href="#" class="btn-outline btn-full">@L["Plan_Enterprise_CTA"]</a>
|
||||
</div>
|
||||
@@ -287,6 +293,8 @@
|
||||
</footer>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? culture { get; set; }
|
||||
|
||||
// Initialize Lucide icons after render
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -309,14 +317,14 @@
|
||||
];
|
||||
|
||||
// Industry cards data (5 industries matching Pencil design)
|
||||
private record IndustryItem(string Title, string Desc, string Chips);
|
||||
private record IndustryItem(string Title, string Desc, string Chips, string Image);
|
||||
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_Restaurant_Title", "Industry_Restaurant_Desc", "Industry_Restaurant_Chips", "/images/home/fnb-ai.png"),
|
||||
new("Industry_Bar_Title", "Industry_Bar_Desc", "Industry_Bar_Chips", "/images/home/bar-ai.png"),
|
||||
new("Industry_Karaoke_Title", "Industry_Karaoke_Desc", "Industry_Karaoke_Chips", "/images/home/karaoke-ai.png"),
|
||||
new("Industry_Coffee_Title", "Industry_Coffee_Desc", "Industry_Coffee_Chips", "/images/home/coffee-ai.png"),
|
||||
new("Industry_Spa_Title", "Industry_Spa_Desc", "Industry_Spa_Chips", "/images/home/spa-ai.png"),
|
||||
];
|
||||
|
||||
// Add-on items with icons (matching Pencil design)
|
||||
|
||||
@@ -5,6 +5,7 @@ using WebClientTpos.Client;
|
||||
using WebClientTpos.Client.Localization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Globalization;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
@@ -27,22 +28,23 @@ builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactor
|
||||
// Build the host
|
||||
var host = builder.Build();
|
||||
|
||||
// Initialize Localization Cache
|
||||
// Initialize Localization Cache
|
||||
var cache = host.Services.GetRequiredService<LocalizationCache>();
|
||||
|
||||
// Detect culture from BaseAddress (which is set by <base href> from Server)
|
||||
var baseAddress = builder.HostEnvironment.BaseAddress;
|
||||
var culture = new CultureInfo("en-US"); // Default
|
||||
// Default culture is Vietnamese
|
||||
var culture = new CultureInfo("vi-VN");
|
||||
|
||||
if (baseAddress.Contains("/vi-VN/", StringComparison.OrdinalIgnoreCase))
|
||||
// Try reading saved culture preference from localStorage
|
||||
try
|
||||
{
|
||||
culture = new CultureInfo("vi-VN");
|
||||
}
|
||||
else if (baseAddress.Contains("/vi/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
culture = new CultureInfo("vi-VN");
|
||||
var jsRuntime = host.Services.GetRequiredService<IJSRuntime>();
|
||||
var savedCulture = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", "aPOS_culture");
|
||||
if (!string.IsNullOrEmpty(savedCulture))
|
||||
{
|
||||
try { culture = new CultureInfo(savedCulture); } catch { }
|
||||
}
|
||||
}
|
||||
catch { /* JS not available yet during startup, use default */ }
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
|
||||
@@ -340,21 +340,20 @@ a {
|
||||
|
||||
.hero-mockup {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 400px;
|
||||
max-width: 1000px;
|
||||
margin: var(--space-16) auto 0;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--border-radius-2xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 60px rgba(255, 92, 0, 0.08);
|
||||
}
|
||||
|
||||
.hero-mockup-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ═════════════════════════════════════════════════════════════════════════
|
||||
8. TRUST SECTION
|
||||
═════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -531,25 +530,34 @@ a {
|
||||
}
|
||||
|
||||
.tpos-industry-card {
|
||||
background: var(--bg-surface);
|
||||
background: var(--bg-page);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--space-8);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.25s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tpos-industry-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--brand-gradient);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
.tpos-industry-img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tpos-industry-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tpos-industry-content {
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.tpos-industry-card:hover {
|
||||
@@ -558,10 +566,6 @@ a {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tpos-industry-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tpos-industry-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
@@ -741,18 +745,23 @@ a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tpos-pricing-feature .check-icon {
|
||||
font-size: 1rem;
|
||||
.tpos-pricing-feature .lucide-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
margin-top: 1px;
|
||||
background-color: transparent !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.tpos-pricing-feature .check-icon.green {
|
||||
color: var(--success);
|
||||
.tpos-pricing-feature .lucide-check.green {
|
||||
color: var(--success) !important;
|
||||
stroke: var(--success);
|
||||
}
|
||||
|
||||
.tpos-pricing-feature .check-icon.accent {
|
||||
color: var(--accent-primary);
|
||||
.tpos-pricing-feature .lucide-check.accent {
|
||||
color: var(--accent-primary) !important;
|
||||
stroke: var(--accent-primary);
|
||||
}
|
||||
|
||||
.tpos-pricing-divider {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Reference in New Issue
Block a user