diff --git a/apps/web-client-tpos-net/Dockerfile b/apps/web-client-tpos-net/Dockerfile new file mode 100644 index 00000000..43e99a8d --- /dev/null +++ b/apps/web-client-tpos-net/Dockerfile @@ -0,0 +1,66 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# WebClientTpos Dockerfile +# EN: Multi-stage build for Blazor WebAssembly Hosted +# VI: Multi-stage build cho Blazor WebAssembly Hosted +# ═══════════════════════════════════════════════════════════════════════════════ + +# ═══════════════════════════════════════════════════════════════════════════════ +# Stage 1: Build +# ═══════════════════════════════════════════════════════════════════════════════ +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# EN: Copy solution and project files for layer caching +# VI: Copy solution và project files để cache layers +COPY WebClientTpos.slnx ./ +COPY src/WebClientTpos.Shared/WebClientTpos.Shared.csproj ./src/WebClientTpos.Shared/ +COPY src/WebClientTpos.Client/WebClientTpos.Client.csproj ./src/WebClientTpos.Client/ +COPY src/WebClientTpos.Server/WebClientTpos.Server.csproj ./src/WebClientTpos.Server/ + +# EN: Restore dependencies +# VI: Restore dependencies +RUN dotnet restore + +# EN: Copy source code +# VI: Copy source code +COPY . . + +# EN: Build and publish +# VI: Build và publish +RUN dotnet publish src/WebClientTpos.Server/WebClientTpos.Server.csproj \ + -c Release \ + -o /app/publish \ + --no-restore + +# ═══════════════════════════════════════════════════════════════════════════════ +# Stage 2: Runtime +# ═══════════════════════════════════════════════════════════════════════════════ +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime +WORKDIR /app + +# EN: Create non-root user for security +# VI: Tạo user không phải root để bảo mật +RUN adduser -D -u 1000 appuser && chown -R appuser /app +USER appuser + +# EN: Copy published output +# VI: Copy output đã publish +COPY --from=build /app/publish . + +# EN: Expose port +# VI: Expose port +EXPOSE 8080 + +# EN: Health check +# VI: Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# EN: Set environment variables +# VI: Thiết lập biến môi trường +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_ENVIRONMENT=Production + +# EN: Run the application +# VI: Chạy ứng dụng +ENTRYPOINT ["dotnet", "WebClientTpos.Server.dll"] diff --git a/apps/web-client-tpos-net/README.md b/apps/web-client-tpos-net/README.md new file mode 100644 index 00000000..acbba0c8 --- /dev/null +++ b/apps/web-client-tpos-net/README.md @@ -0,0 +1,79 @@ +# WebClientTpos - Blazor Web App .NET 10 + +> **EN**: [English Documentation](docs/en/README.md) +> **VI**: [Tài liệu Tiếng Việt](docs/vi/README.md) + +Base frontend web application cho GoodGo Platform được xây dựng với Blazor WebAssembly + BFF Pattern. + +## Quick Links + +- 📖 [Architecture](docs/en/ARCHITECTURE.md) / [Kiến trúc](docs/vi/ARCHITECTURE.md) +- 🚀 [Quick Start](docs/en/README.md#quick-start) +- 🔧 [Configuration](docs/en/README.md#configuration) + +## Architecture / Kiến trúc + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Browser │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Blazor WebAssembly Client │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────┬────────────────────────────────────┘ + │ /api/* + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ BFF (Backend for Frontend) │ +│ WebClientTpos.Server + YARP │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Routes: /api/iam/** → iam-service │ │ +│ │ /api/merchants/** → merchant-service │ │ +│ │ /api/catalog/** → catalog-service │ │ +│ │ /api/orders/** → order-service │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────┬────────────────────────────────────┘ + │ Internal Network + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Microservices │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ IAM │ │ Merchant │ │ Catalog │ │ Order │ │ +│ │ :5101 │ │ :5102 │ │ :5103 │ │ :5104 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Client | Blazor WebAssembly (.NET 10) | +| BFF | ASP.NET Core + YARP Reverse Proxy | +| Shared | Class Library với Data Annotations | +| Styling | CSS Variables, Dark Mode | + +## Getting Started / Bắt đầu + +```bash +cd apps/web-client-base-net +dotnet restore +dotnet run --project src/WebClientTpos.Server + +# Open http://localhost:5091 +``` + +## Project Structure / Cấu trúc + +``` +web-client-base-net/ +├── src/ +│ ├── WebClientTpos.Client/ # Blazor WebAssembly +│ ├── WebClientTpos.Server/ # BFF with YARP Proxy +│ └── WebClientTpos.Shared/ # Shared DTOs +├── docs/ +│ ├── en/ # English docs +│ └── vi/ # Vietnamese docs +└── Dockerfile +``` + +See detailed documentation in [docs/en/](docs/en/) or [docs/vi/](docs/vi/). diff --git a/apps/web-client-tpos-net/WebClientTpos.slnx b/apps/web-client-tpos-net/WebClientTpos.slnx new file mode 100644 index 00000000..e8467f27 --- /dev/null +++ b/apps/web-client-tpos-net/WebClientTpos.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web-client-tpos-net/docs/en/ARCHITECTURE.md b/apps/web-client-tpos-net/docs/en/ARCHITECTURE.md new file mode 100644 index 00000000..f3a28b95 --- /dev/null +++ b/apps/web-client-tpos-net/docs/en/ARCHITECTURE.md @@ -0,0 +1,99 @@ +# WebClientTpos Architecture + +Base frontend architecture for GoodGo Platform using Blazor WebAssembly with BFF Pattern. + +## Overview + +This application implements the **Backend for Frontend (BFF)** pattern with YARP reverse proxy: + +- **Client**: Blazor WebAssembly running in browser +- **BFF**: ASP.NET Core server proxying requests to microservices +- **Shared**: Common DTOs with validation (shared between Client/BFF) + +## Architecture Diagram + +```mermaid +flowchart TD + subgraph Browser["🌐 Browser"] + C[Blazor WASM Client] + end + + subgraph BFF["⚙️ BFF Layer"] + Y[YARP Reverse Proxy] + SA[Static Assets] + end + + subgraph MS["🔧 Microservices"] + IAM[IAM Service] + MER[Merchant Service] + CAT[Catalog Service] + ORD[Order Service] + end + + C -->|Static Files| SA + C -->|/api/*| Y + Y -->|/api/iam/**| IAM + Y -->|/api/merchants/**| MER + Y -->|/api/catalog/**| CAT + Y -->|/api/orders/**| ORD + + style C fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px + style Y fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px + style SA fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px + style IAM fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style MER fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style CAT fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style ORD fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px +``` + +## Project Structure + +``` +web-client-base-net/ +├── src/ +│ ├── WebClientTpos.Client/ # Blazor WebAssembly +│ │ ├── Layout/ # MainLayout, NavMenu +│ │ ├── Pages/ # Razor pages +│ │ └── wwwroot/ # Static assets, CSS +│ ├── WebClientTpos.Server/ # BFF + YARP Proxy +│ │ ├── Program.cs # App configuration +│ │ └── yarp.json # Proxy routes +│ └── WebClientTpos.Shared/ # Shared DTOs +│ └── DTOs/ # ProductDto, UserDto +└── docs/ +``` + +## Shared DTOs + +| DTO | Purpose | Validation | +|-----|---------|------------| +| `ProductDto` | Product data | Name (3-100 chars), Price (0.01-1B) | +| `RegisterDto` | User registration | Email, Password (8+ chars, complexity) | +| `LoginDto` | User login | Email, Password | +| `ApiResponse` | Standard response wrapper | Success, Data, Error | + +## YARP Routes + +| Route | Target Service | Port | +|-------|---------------|------| +| `/api/iam/**` | IAM Service | 5101 | +| `/api/merchants/**` | Merchant Service | 5102 | +| `/api/catalog/**` | Catalog Service | 5103 | +| `/api/orders/**` | Order Service | 5104 | + +## Blazor Pages + +| Page | Route | Description | +|------|-------|-------------| +| Home | `/` | Landing page | +| Products | `/products` | Product management | +| Auth | `/auth` | Login/Register | +| Counter | `/counter` | Demo component | +| Weather | `/weather` | Demo data fetch | + +## Key Technologies + +- **.NET 10** - Latest framework +- **Blazor WebAssembly** - Client-side SPA +- **YARP** - Reverse proxy for microservices +- **Data Annotations** - Shared validation diff --git a/apps/web-client-tpos-net/docs/en/README.md b/apps/web-client-tpos-net/docs/en/README.md new file mode 100644 index 00000000..d61dbd3b --- /dev/null +++ b/apps/web-client-tpos-net/docs/en/README.md @@ -0,0 +1,65 @@ +# WebClientTpos + +Base frontend application cho GoodGo Platform. + +## Quick Start + +```bash +# Navigate to project +cd apps/web-client-base-net + +# Restore packages +dotnet restore + +# Run BFF server +dotnet run --project src/WebClientTpos.Server + +# Open browser at http://localhost:5091 +``` + +## Tech Stack + +| Component | Technology | +|-----------|------------| +| Client | Blazor WebAssembly (.NET 10) | +| BFF | ASP.NET Core + YARP | +| Shared | Class Library with Data Annotations | +| Styling | CSS Variables, Dark Mode | + +## Project Structure + +``` +src/ +├── WebClientTpos.Client/ # Blazor WASM frontend +├── WebClientTpos.Server/ # BFF with YARP proxy +└── WebClientTpos.Shared/ # Shared DTOs +``` + +## Features + +- ✅ BFF Pattern with YARP reverse proxy +- ✅ Shared validation (Client + Server) +- ✅ Dark/Light mode support +- ✅ Glassmorphism UI design +- ✅ Health check endpoint `/health` + +## Configuration + +YARP proxy routes in `yarp.json`: + +```json +{ + "ReverseProxy": { + "Routes": { + "iam-route": { "Match": { "Path": "/api/iam/{**catch-all}" } } + }, + "Clusters": { + "iam-cluster": { "Destinations": { "d1": { "Address": "http://localhost:5101" } } } + } + } +} +``` + +## Related Documentation + +- [Architecture](ARCHITECTURE.md) - System architecture details diff --git a/apps/web-client-tpos-net/docs/vi/ARCHITECTURE.md b/apps/web-client-tpos-net/docs/vi/ARCHITECTURE.md new file mode 100644 index 00000000..1ea110a9 --- /dev/null +++ b/apps/web-client-tpos-net/docs/vi/ARCHITECTURE.md @@ -0,0 +1,99 @@ +# Kiến trúc WebClientTpos + +Kiến trúc frontend cơ sở cho GoodGo Platform sử dụng Blazor WebAssembly với mô hình BFF. + +## Tổng quan + +Ứng dụng này triển khai mô hình **Backend for Frontend (BFF)** với YARP reverse proxy: + +- **Client**: Blazor WebAssembly chạy trên trình duyệt +- **BFF**: ASP.NET Core server proxy yêu cầu đến microservices +- **Shared**: DTOs chung với validation (chia sẻ giữa Client/BFF) + +## Sơ đồ Kiến trúc + +```mermaid +flowchart TD + subgraph Browser["🌐 Trình duyệt"] + C[Blazor WASM Client] + end + + subgraph BFF["⚙️ Lớp BFF"] + Y[YARP Reverse Proxy] + SA[Tài nguyên tĩnh] + end + + subgraph MS["🔧 Microservices"] + IAM[Dịch vụ IAM] + MER[Dịch vụ Merchant] + CAT[Dịch vụ Catalog] + ORD[Dịch vụ Order] + end + + C -->|Tệp tĩnh| SA + C -->|/api/*| Y + Y -->|/api/iam/**| IAM + Y -->|/api/merchants/**| MER + Y -->|/api/catalog/**| CAT + Y -->|/api/orders/**| ORD + + style C fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px + style Y fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px + style SA fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px + style IAM fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style MER fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style CAT fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style ORD fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px +``` + +## Cấu trúc Dự án + +``` +web-client-base-net/ +├── src/ +│ ├── WebClientTpos.Client/ # Blazor WebAssembly +│ │ ├── Layout/ # MainLayout, NavMenu +│ │ ├── Pages/ # Trang Razor +│ │ └── wwwroot/ # Tài nguyên tĩnh, CSS +│ ├── WebClientTpos.Server/ # BFF + YARP Proxy +│ │ ├── Program.cs # Cấu hình ứng dụng +│ │ └── yarp.json # Routes proxy +│ └── WebClientTpos.Shared/ # DTOs chung +│ └── DTOs/ # ProductDto, UserDto +└── docs/ +``` + +## Shared DTOs + +| DTO | Mục đích | Validation | +|-----|----------|------------| +| `ProductDto` | Dữ liệu sản phẩm | Tên (3-100 ký tự), Giá (0.01-1 tỷ) | +| `RegisterDto` | Đăng ký người dùng | Email, Mật khẩu (8+ ký tự, độ phức tạp) | +| `LoginDto` | Đăng nhập | Email, Mật khẩu | +| `ApiResponse` | Wrapper response chuẩn | Success, Data, Error | + +## YARP Routes + +| Route | Dịch vụ đích | Cổng | +|-------|-------------|------| +| `/api/iam/**` | Dịch vụ IAM | 5101 | +| `/api/merchants/**` | Dịch vụ Merchant | 5102 | +| `/api/catalog/**` | Dịch vụ Catalog | 5103 | +| `/api/orders/**` | Dịch vụ Order | 5104 | + +## Trang Blazor + +| Trang | Route | Mô tả | +|-------|-------|-------| +| Home | `/` | Trang chủ | +| Products | `/products` | Quản lý sản phẩm | +| Auth | `/auth` | Đăng nhập/Đăng ký | +| Counter | `/counter` | Component demo | +| Weather | `/weather` | Demo fetch dữ liệu | + +## Công nghệ chính + +- **.NET 10** - Framework mới nhất +- **Blazor WebAssembly** - SPA phía client +- **YARP** - Reverse proxy cho microservices +- **Data Annotations** - Validation chia sẻ diff --git a/apps/web-client-tpos-net/docs/vi/README.md b/apps/web-client-tpos-net/docs/vi/README.md new file mode 100644 index 00000000..3f0b6d01 --- /dev/null +++ b/apps/web-client-tpos-net/docs/vi/README.md @@ -0,0 +1,65 @@ +# WebClientTpos + +Ứng dụng frontend cơ sở cho GoodGo Platform. + +## Bắt đầu nhanh + +```bash +# Di chuyển đến dự án +cd apps/web-client-base-net + +# Khôi phục packages +dotnet restore + +# Chạy BFF server +dotnet run --project src/WebClientTpos.Server + +# Mở trình duyệt tại http://localhost:5091 +``` + +## Công nghệ + +| Thành phần | Công nghệ | +|------------|-----------| +| Client | Blazor WebAssembly (.NET 10) | +| BFF | ASP.NET Core + YARP | +| Shared | Class Library với Data Annotations | +| Styling | CSS Variables, Dark Mode | + +## Cấu trúc Dự án + +``` +src/ +├── WebClientTpos.Client/ # Blazor WASM frontend +├── WebClientTpos.Server/ # BFF với YARP proxy +└── WebClientTpos.Shared/ # DTOs chia sẻ +``` + +## Tính năng + +- ✅ Mô hình BFF với YARP reverse proxy +- ✅ Validation chia sẻ (Client + Server) +- ✅ Hỗ trợ chế độ Sáng/Tối +- ✅ Thiết kế UI Glassmorphism +- ✅ Endpoint kiểm tra sức khỏe `/health` + +## Cấu hình + +Routes YARP proxy trong `yarp.json`: + +```json +{ + "ReverseProxy": { + "Routes": { + "iam-route": { "Match": { "Path": "/api/iam/{**catch-all}" } } + }, + "Clusters": { + "iam-cluster": { "Destinations": { "d1": { "Address": "http://localhost:5101" } } } + } + } +} +``` + +## Tài liệu liên quan + +- [Kiến trúc](ARCHITECTURE.md) - Chi tiết kiến trúc hệ thống diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/App.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/App.razor new file mode 100644 index 00000000..a8a79e51 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/App.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/LanguageSwitcher.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/LanguageSwitcher.razor new file mode 100644 index 00000000..1325ee47 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/LanguageSwitcher.razor @@ -0,0 +1,67 @@ +@using System.Globalization +@inject NavigationManager Navigation + + + + + + @GetCurrentLabel() + + + + + + + + 🇻🇳 + Tiếng Việt + + + + + 🇺🇸 + English + + + + + +@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"; + } + + private void SwitchLanguage(string targetCulture) + { + var uri = new Uri(Navigation.Uri); + var path = uri.PathAndQuery; + + 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))) + { + segments[0] = targetCulture; + newPath = "/" + string.Join('/', segments); + } + else + { + if (path == "/") path = ""; + newPath = $"/{targetCulture}{path}"; + } + + Navigation.NavigateTo(newPath, forceLoad: true); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor new file mode 100644 index 00000000..327858c3 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor @@ -0,0 +1,86 @@ +@inherits LayoutComponentBase +@inject IStringLocalizer L + + + + + + + + + + +@if (_mobileMenuOpen) +{ +
+ +} + + + + + @Body + + + +@code { + private bool _mobileMenuOpen = false; + + private void ToggleMobileMenu() => _mobileMenuOpen = !_mobileMenuOpen; + private void CloseMobileMenu() => _mobileMenuOpen = false; + + private MudTheme _theme = new() + { + PaletteDark = new PaletteDark() + { + Primary = "#FF5C00", + PrimaryContrastText = "#FFFFFF", + AppbarBackground = "rgba(10,10,11,0.85)", + AppbarText = "#FFFFFF", + Background = "#0A0A0B", + Surface = "#111113", + TextPrimary = "#FFFFFF", + TextSecondary = "#ADADB0", + ActionDefault = "#FFFFFF", + LinesDefault = "#1F1F23" + } + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor.css new file mode 100644 index 00000000..253a1222 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor.css @@ -0,0 +1,3 @@ +/* MainLayout scoped styles - aPOS */ +/* The main layout uses global CSS classes from app.css */ +/* This file is kept minimal as aPOS styling is handled globally */ \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor new file mode 100644 index 00000000..e0887b8d --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor @@ -0,0 +1,17 @@ + + + Home + + + Products + + + Auth + + + Counter + + + Weather + + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor.css new file mode 100644 index 00000000..617b89cc --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/NavMenu.razor.css @@ -0,0 +1,83 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizer.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizer.cs new file mode 100644 index 00000000..f81deb35 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizer.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Net.Http.Json; +using Microsoft.Extensions.Localization; + +namespace WebClientTpos.Client.Localization; + +public class JsonStringLocalizer : IStringLocalizer +{ + private readonly LocalizationCache _cache; + private readonly string _resourceName; + + public JsonStringLocalizer(LocalizationCache cache, string resourceName) + { + _cache = cache; + _resourceName = resourceName; + } + + // This constructor style is used by the Factory (if we update factory) + public JsonStringLocalizer(LocalizationCache cache) + { + _cache = cache; + _resourceName = "Shared"; + } + + public LocalizedString this[string name] + { + get + { + var value = GetString(name); + return new LocalizedString(name, value ?? name, resourceNotFound: value == null); + } + } + + public LocalizedString this[string name, params object[] arguments] + { + get + { + var format = GetString(name); + var value = string.Format(format ?? name, arguments); + return new LocalizedString(name, value, resourceNotFound: format == null); + } + } + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + // Not fully supported by simple cache + return Enumerable.Empty(); + } + + private string? GetString(string name) + { + return _cache.GetString(name); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizerFactory.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizerFactory.cs new file mode 100644 index 00000000..2ebf9578 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizerFactory.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Localization; + +namespace WebClientTpos.Client.Localization; + +public class JsonStringLocalizerFactory : IStringLocalizerFactory +{ + private readonly LocalizationCache _cache; + private readonly IServiceProvider _serviceProvider; + + public JsonStringLocalizerFactory(LocalizationCache cache, IServiceProvider serviceProvider) + { + _cache = cache; + _serviceProvider = serviceProvider; + } + + public IStringLocalizer Create(Type resourceSource) + { + return new JsonStringLocalizer(_cache, resourceSource.Name); + } + + public IStringLocalizer Create(string baseName, string location) + { + return new JsonStringLocalizer(_cache, baseName); + } +} + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/LocalizationCache.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/LocalizationCache.cs new file mode 100644 index 00000000..98f64201 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/LocalizationCache.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using System.Net.Http.Json; + +namespace WebClientTpos.Client.Localization; + +public class LocalizationCache +{ + private readonly HttpClient _httpClient; + private Dictionary _strings = new(); + private bool _isLoaded; + + public LocalizationCache(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public string? GetString(string key) + { + if (_strings.TryGetValue(key, out var value)) + { + return value; + } + return null; + } + + public async Task LoadAsync(CultureInfo culture) + { + if (_isLoaded) return; // Or check if culture changed + + 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>($"/locales/{cultureName}.json?v={DateTime.Now.Ticks}"); + if (loaded != null) + { + _strings = loaded; + _isLoaded = true; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading localization for {culture.Name}: {ex.Message}"); + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor new file mode 100644 index 00000000..7315f14d --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor @@ -0,0 +1,108 @@ +@page "/forgot-password" +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Shared +@inject HttpClient Http +@inject IStringLocalizer L + +@* + EN: Forgot password page. + VI: Trang quên mật khẩu. +*@ + +@L["Auth_ForgotPassword_Title"] + +
+
+

@L["Auth_ForgotPassword_Title"]

+

@L["Auth_ForgotPassword_Subtitle"]

+ + + + +
+ + + +
+ + +
+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + + +
+
+ +@code { + private ForgotPasswordDto forgotPasswordModel = new(); + private bool isSubmitting = false; + private string message = ""; + private bool success = false; + + /// + /// EN: Handle forgot password form submission. + /// VI: Xử lý submit form quên mật khẩu. + /// + 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>(); + 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; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Login.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Login.razor new file mode 100644 index 00000000..924e25b5 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Login.razor @@ -0,0 +1,136 @@ +@page "/login" +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Shared +@inject HttpClient Http +@inject NavigationManager Navigation +@inject IStringLocalizer L + +@* + EN: Login page with email/password authentication. + VI: Trang đăng nhập với xác thực email/mật khẩu. +*@ + +@L["Auth_Login_Title"] + +
+
+

@L["Auth_Login_Title"]

+

@L["Auth_Login_Subtitle"]

+ + + + +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+ + @L["Auth_Login_ForgotPassword"] +
+ + +
+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + + +
+
+ +@code { + private LoginDto loginModel = new(); + private bool isSubmitting = false; + private string message = ""; + private bool success = false; + + /// + /// EN: Handle login form submission. + /// VI: Xử lý submit form đăng nhập. + /// + 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>(); + 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; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor new file mode 100644 index 00000000..083bca6a --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor @@ -0,0 +1,295 @@ +@page "/profile" +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Shared +@inject HttpClient Http +@inject NavigationManager Navigation +@inject IStringLocalizer 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). +*@ + +@L["Auth_Profile_Title"] + +
+
+

@L["Auth_Profile_Title"]

+

@L["Auth_Profile_Subtitle"]

+
+ + @if (isLoading) + { +
+ +

@L["Common_Loading"]

+
+ } + else if (userProfile != null) + { +
+ +
+

@L["Auth_Profile_PersonalInfo"]

+ + + + +
+ + +
+ +
+ + +
+ +
+ +

@userProfile.CreatedAt.ToString("MMMM dd, yyyy")

+
+ + @if (!isEditingProfile) + { + + } + else + { +
+ + +
+ } +
+
+ + +
+

@L["Auth_Profile_Security"]

+ + + + +
+ + + +
+ +
+ + + @L["Auth_Register_PasswordHint"] + +
+ +
+ + + +
+ + +
+
+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + +
+ +
+
+ } +
+ +@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(); + } + + /// + /// EN: Load user profile data. + /// VI: Tải dữ liệu hồ sơ người dùng. + /// + private async Task LoadUserProfile() + { + try + { + var response = await Http.GetAsync("api/auth/profile"); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync>(); + 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; + } + } + + /// + /// EN: Handle profile update. + /// VI: Xử lý cập nhật hồ sơ. + /// + 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; + } + } + + /// + /// EN: Handle password change. + /// VI: Xử lý đổi mật khẩu. + /// + 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; + } + } + + /// + /// EN: Cancel profile editing. + /// VI: Hủy chỉnh sửa hồ sơ. + /// + private async Task CancelEditProfile() + { + isEditingProfile = false; + // Reload to reset changes + await LoadUserProfile(); + } + + /// + /// EN: Handle user logout. + /// VI: Xử lý đăng xuất. + /// + 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"); + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor new file mode 100644 index 00000000..c9e24991 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor @@ -0,0 +1,155 @@ +@page "/register" +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Shared +@inject HttpClient Http +@inject NavigationManager Navigation +@inject IStringLocalizer L + +@* + EN: User registration page. + VI: Trang đăng ký người dùng. +*@ + +@L["Auth_Register_Title"] + +
+
+

@L["Auth_Register_Title"]

+

@L["Auth_Register_Subtitle"]

+ + + + +
+ + + +
+ +
+ + + +
+ +
+ + + @L["Auth_Register_PasswordHint"] + +
+ +
+ + + +
+ +
+ + +
+ + +
+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + + +
+
+ +@code { + private RegisterDto registerModel = new(); + private bool isSubmitting = false; + private string message = ""; + private bool success = false; + + /// + /// EN: Handle registration form submission. + /// VI: Xử lý submit form đăng ký. + /// + 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>(); + 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; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor new file mode 100644 index 00000000..4de25acc --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor @@ -0,0 +1,164 @@ +@page "/reset-password" +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Shared +@inject HttpClient Http +@inject NavigationManager Navigation +@inject IStringLocalizer L + +@* + EN: Reset password page with token validation. + VI: Trang đặt lại mật khẩu với xác thực token. +*@ + +@L["Auth_ResetPassword_Title"] + +
+
+

@L["Auth_ResetPassword_Title"]

+

@L["Auth_ResetPassword_Subtitle"]

+ + @if (invalidToken) + { +
+ @L["Auth_ResetPassword_InvalidToken"] +
+ + } + else + { + + + +
+ + + @L["Auth_ResetPassword_PasswordHint"] + +
+ +
+ + + +
+ + +
+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + } +
+
+ +@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; + } + } + + /// + /// EN: Handle reset password form submission. + /// VI: Xử lý submit form đặt lại mật khẩu. + /// + 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>(); + 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; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor new file mode 100644 index 00000000..9b7cc284 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor @@ -0,0 +1,124 @@ +@page "/verify-email" +@using WebClientTpos.Shared +@inject HttpClient Http +@inject NavigationManager Navigation +@inject IStringLocalizer L + +@* + EN: Email verification page. + VI: Trang xác minh email. +*@ + +@L["Auth_VerifyEmail_Title"] + +
+
+

@L["Auth_VerifyEmail_Title"]

+ + @if (isVerifying) + { +
+ +

@L["Auth_VerifyEmail_Verifying"]

+
+ } + else if (success) + { +
+ + + +

@L["Auth_VerifyEmail_Success"]

+ +
+ } + else + { +
+ @message +
+ + } +
+
+ +@code { + private bool isVerifying = true; + private bool success = false; + private string message = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await VerifyEmailAsync(); + } + } + + /// + /// EN: Verify email using token from query parameters. + /// VI: Xác minh email sử dụng token từ query parameters. + /// + 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>(); + 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(); + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor new file mode 100644 index 00000000..456d7c29 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor @@ -0,0 +1,302 @@ +@page "/" +@inject IStringLocalizer L + +aPOS - @L["HeroHeadline"] + + +
+
@((MarkupString)L["HeroBadge"].Value)
+ +

+ @((MarkupString)L["HeroHeadline"].Value) +

+ +

+ @L["HeroSubtext"] +

+ + + +
+ @L["HeroMockup_Alt"] +
+
+ + +
+
+

@L["Trust_Label"]

+
+ @((MarkupString)L["Trust_Stat1"].Value) + @((MarkupString)L["Trust_Stat2"].Value) + @((MarkupString)L["Trust_Stat3"].Value) +
+
+
+ + +
+
+
+
@L["Features_Badge"]
+

@L["Features_Title"]

+

@L["Features_Desc"]

+
+ +
+ @foreach (var f in _features) + { +
+
@((MarkupString)L[f.Icon].Value)
+

@L[f.Title]

+

@L[f.Desc]

+
+ } +
+
+
+ + +
+
+
+
@L["Industries_Badge"]
+

@L["Industries_Title"]

+

@L["Industries_Desc"]

+
+ +
+ @foreach (var ind in _industries) + { +
+

@L[ind.Title]

+

@L[ind.Desc]

+
+ @foreach (var chip in L[ind.Chips].Value.Split(',')) + { + ✨ @chip.Trim() + } +
+
+ } +
+
+
+ + +
+
+
+
@L["Steps_Badge"]
+

@L["Steps_Title"]

+

@L["Steps_Desc"]

+
+ +
+
+
1
+

@L["Step1_Title"]

+

@L["Step1_Desc"]

+
+
+
2
+

@L["Step2_Title"]

+

@L["Step2_Desc"]

+
+
+
3
+

@L["Step3_Title"]

+

@L["Step3_Desc"]

+
+
+
+
+ + +
+
+
+
@L["Pricing_Badge"]
+

@L["Pricing_Title"]

+

@L["Pricing_Desc"]

+
+ +
+ +
+
@L["Plan_Starter_Badge"]
+
@L["Plan_Starter_Name"]
+
+ @L["Plan_Starter_Price"] + @L["Plan_Starter_Period"] +
+

@L["Plan_Starter_Desc"]

+
    +
  • @L["Plan_Starter_Feature1"]
  • +
  • @L["Plan_Starter_Feature2"]
  • +
  • @L["Plan_Starter_Feature3"]
  • +
  • @L["Plan_Starter_Feature4"]
  • +
+ @L["Plan_Starter_CTA"] +
+ + + + + +
+
@L["Plan_Enterprise_Badge"]
+
@L["Plan_Enterprise_Name"]
+
+ @L["Plan_Enterprise_Price"] + @L["Plan_Enterprise_Period"] +
+

@L["Plan_Enterprise_Desc"]

+
    +
  • @L["Plan_Enterprise_Feature1"]
  • +
  • @L["Plan_Enterprise_Feature2"]
  • +
  • @L["Plan_Enterprise_Feature3"]
  • +
  • @L["Plan_Enterprise_Feature4"]
  • +
  • @L["Plan_Enterprise_Feature5"]
  • +
+ @L["Plan_Enterprise_CTA"] +
+
+ + +
+

@L["Addons_Title"]

+
+ @foreach (var addon in _addons) + { +
+
@L[addon.Name]
+
@L[addon.Price]
+
+ } +
+
+
+
+ + +
+

@L["CTA_Title"]

+

@L["CTA_Subtitle"]

+ +

@L["CTA_Trust"]

+
+ + + + +@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"), + ]; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/NotFound.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/NotFound.razor new file mode 100644 index 00000000..917ada1d --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs new file mode 100644 index 00000000..264318ac --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Program.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using WebClientTpos.Client; +using WebClientTpos.Client.Localization; +using Microsoft.Extensions.Localization; +using System.Globalization; + + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +// EN: Add HttpClient for API calls +// VI: Thêm HttpClient cho các cuộc gọi API +builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress).GetLeftPart(UriPartial.Authority)) }); + +// EN: Add MudBlazor services +// VI: Thêm các services của MudBlazor +builder.Services.AddMudServices(); + +// Localization +builder.Services.AddLocalization(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Build the host +var host = builder.Build(); + +// Initialize Localization Cache +// Initialize Localization Cache +var cache = host.Services.GetRequiredService(); + +// Detect culture from BaseAddress (which is set by from Server) +var baseAddress = builder.HostEnvironment.BaseAddress; +var culture = new CultureInfo("en-US"); // Default + +if (baseAddress.Contains("/vi-VN/", StringComparison.OrdinalIgnoreCase)) +{ + culture = new CultureInfo("vi-VN"); +} +else if (baseAddress.Contains("/vi/", StringComparison.OrdinalIgnoreCase)) +{ + culture = new CultureInfo("vi-VN"); +} + +CultureInfo.DefaultThreadCurrentCulture = culture; +CultureInfo.DefaultThreadCurrentUICulture = culture; + +await cache.LoadAsync(culture); + +await host.RunAsync(); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Properties/launchSettings.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/Properties/launchSettings.json new file mode 100644 index 00000000..cbfca635 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5043", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7002;http://localhost:5043", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/WebClientTpos.Client.csproj b/apps/web-client-tpos-net/src/WebClientTpos.Client/WebClientTpos.Client.csproj new file mode 100644 index 00000000..539c6fc8 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/WebClientTpos.Client.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + true + true + + + + + + + + + + + + + + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/_Imports.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/_Imports.razor new file mode 100644 index 00000000..25dadb36 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/_Imports.razor @@ -0,0 +1,16 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using MudBlazor +@using WebClientTpos.Client +@using WebClientTpos.Client.Layout +@using WebClientTpos.Shared +@using WebClientTpos.Shared.DTOs +@using WebClientTpos.Client.Components +@using Microsoft.Extensions.Localization + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/app.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/app.css new file mode 100644 index 00000000..37bdbe81 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/app.css @@ -0,0 +1,1563 @@ +/* ═══════════════════════════════════════════════════════════════════════════════ + aPOS Design System - Dark Theme + Orange Accent + Architecture: Primitives -> Semantics -> Components + Based on: pencil-design/src/pages/aPOS/landing/ tokens + ═══════════════════════════════════════════════════════════════════════════════ */ + +:root { + /* ═════════════════════════════════════════════════════════════════════════ + 1. PRIMITIVES (Raw Color Palettes) + Do NOT use these directly in components. Use Semantic Tokens instead. + ═════════════════════════════════════════════════════════════════════════ */ + + /* Neutral (Dark Scale) */ + --primitive-neutral-0: #ffffff; + --primitive-neutral-50: #fafafa; + --primitive-neutral-100: #ADADB0; + --primitive-neutral-200: #8B8B90; + --primitive-neutral-300: #6B6B70; + --primitive-neutral-400: #3A3A3E; + --primitive-neutral-500: #2A2A2E; + --primitive-neutral-600: #1F1F23; + --primitive-neutral-700: #1A1A1D; + --primitive-neutral-800: #111113; + --primitive-neutral-900: #0A0A0B; + --primitive-neutral-950: #050506; + + /* Accent (Orange) */ + --primitive-accent-400: #FF8A4C; + --primitive-accent-500: #FF5C00; + --primitive-accent-600: #E05200; + + /* Success (Green) */ + --primitive-success-500: #22C55E; + + /* Utility */ + --primitive-white: #ffffff; + --primitive-black: #000000; + --primitive-overlay: rgba(0, 0, 0, 0.6); + + /* Typography */ + --font-heading: 'Inter', system-ui, sans-serif; + --font-body: 'Inter', system-ui, sans-serif; + + /* Spacing Scale */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + --space-20: 5rem; + --space-24: 6rem; + + /* ═════════════════════════════════════════════════════════════════════════ + 2. SEMANTIC TOKENS (Dark Mode Default - aPOS is always dark) + Use these in your CSS. + ═════════════════════════════════════════════════════════════════════════ */ + + /* Backgrounds */ + --bg-page: #0A0A0B; + --bg-surface: #111113; + --bg-elevated: #1A1A1D; + --bg-interactive: #2A2A2E; + --bg-surface-hover: #1F1F23; + --bg-overlay: rgba(10, 10, 11, 0.9); + + /* Gradients */ + --brand-gradient: linear-gradient(135deg, #FF5C00 0%, #FF8A4C 100%); + --surface-gradient: linear-gradient(180deg, #111113 0%, #0A0A0B 100%); + + /* Text */ + --text-primary: #FFFFFF; + --text-secondary: #ADADB0; + --text-tertiary: #8B8B90; + --text-disabled: #6B6B70; + --text-muted: rgba(255, 255, 255, 0.8); + --text-inverse: #0A0A0B; + + /* Accent */ + --accent-primary: #FF5C00; + --accent-light: #FF8A4C; + --accent-glow: rgba(255, 92, 0, 0.15); + --accent-glow-strong: rgba(255, 92, 0, 0.3); + + /* Borders */ + --border-subtle: #1F1F23; + --border-default: #2A2A2E; + --border-strong: #3A3A3E; + + /* Actions */ + --action-primary-bg: #FF5C00; + --action-primary-bg-hover: #E05200; + --action-primary-text: #FFFFFF; + + --action-secondary-bg: transparent; + --action-secondary-bg-hover: rgba(255, 255, 255, 0.05); + --action-secondary-text: #FFFFFF; + --action-secondary-border: #2A2A2E; + + /* Status */ + --success: #22C55E; + --border-radius-base: 6px; + --border-radius-lg: 10px; + --border-radius-xl: 14px; + --border-radius-2xl: 20px; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 3. aPOS IS ALWAYS DARK — No light/dark toggle needed + ═════════════════════════════════════════════════════════════════════════ */ + +/* ═════════════════════════════════════════════════════════════════════════ + 4. GLOBAL RESET & BASE STYLES + ═════════════════════════════════════════════════════════════════════════ */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +html, +body { + margin: 0; + padding: 0; + font-family: var(--font-body); + background-color: var(--bg-page) !important; + color: var(--text-primary) !important; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-heading); + color: var(--text-primary); + margin-top: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-6); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 5. NAVIGATION (aPOS Header) + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-navbar { + position: sticky; + top: 0; + z-index: 1000; + background: rgba(10, 10, 11, 0.85); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-subtle); + padding: var(--space-4) 0; +} + +.tpos-navbar-inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-6); + display: flex; + align-items: center; + justify-content: space-between; +} + +.tpos-logo { + font-size: 1.5rem; + font-weight: 800; + color: var(--accent-primary); + letter-spacing: -0.02em; +} + +.tpos-nav-links { + display: flex; + gap: var(--space-8); + align-items: center; +} + +.tpos-nav-link { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + transition: color 0.2s ease; +} + +.tpos-nav-link:hover { + color: var(--text-primary); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 6. BUTTONS (aPOS) + ═════════════════════════════════════════════════════════════════════════ */ +.btn-accent { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + background: var(--accent-primary); + color: #fff; + padding: var(--space-3) var(--space-6); + border-radius: var(--border-radius-lg); + font-weight: 600; + font-size: 0.9375rem; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.btn-accent:hover { + background: var(--action-primary-bg-hover); + transform: translateY(-1px); + box-shadow: 0 4px 20px var(--accent-glow-strong); +} + +.btn-accent-lg { + padding: var(--space-4) var(--space-8); + font-size: 1rem; + border-radius: var(--border-radius-xl); +} + +.btn-outline { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + background: transparent; + color: var(--text-primary); + padding: var(--space-3) var(--space-6); + border-radius: var(--border-radius-lg); + font-weight: 600; + font-size: 0.9375rem; + border: 1px solid var(--border-default); + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.btn-outline:hover { + background: rgba(255, 255, 255, 0.05); + border-color: var(--border-strong); +} + +.btn-outline-lg { + padding: var(--space-4) var(--space-8); + font-size: 1rem; + border-radius: var(--border-radius-xl); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 7. HERO SECTION + ═════════════════════════════════════════════════════════════════════════ */ +.hero-section { + padding: var(--space-20) var(--space-6) var(--space-16); + text-align: center; + max-width: 900px; + margin: 0 auto; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: var(--space-2); + background: var(--accent-glow); + border: 1px solid rgba(255, 92, 0, 0.25); + color: var(--accent-light); + padding: var(--space-2) var(--space-4); + border-radius: 999px; + font-size: 0.8125rem; + font-weight: 600; + margin-bottom: var(--space-8); + letter-spacing: 0.02em; +} + +.hero-headline { + font-size: 3.5rem; + font-weight: 800; + line-height: 1.08; + margin-bottom: var(--space-6); + letter-spacing: -0.03em; + background: linear-gradient(180deg, #FFFFFF 30%, #ADADB0 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtext { + font-size: 1.125rem; + color: var(--text-secondary); + max-width: 640px; + margin: 0 auto var(--space-10); + line-height: 1.7; +} + +.hero-actions { + display: flex; + gap: var(--space-4); + justify-content: center; + flex-wrap: wrap; +} + +.hero-mockup { + width: 100%; + max-width: 900px; + height: 400px; + margin: var(--space-16) auto 0; + background: var(--bg-surface); + border: 1px solid var(--border-default); + 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); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 8. TRUST SECTION + ═════════════════════════════════════════════════════════════════════════ */ +.trust-section { + padding: var(--space-12) 0; + text-align: center; + border-top: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-subtle); +} + +.trust-label { + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); + margin-bottom: var(--space-6); +} + +.trust-stats { + display: flex; + justify-content: center; + gap: var(--space-12); + flex-wrap: wrap; +} + +.trust-stat { + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); +} + +.trust-stat strong { + color: var(--accent-primary); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 9. SECTION HEADER (Badge + Title + Description) + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-section { + padding: var(--space-20) 0; +} + +.tpos-section-header { + text-align: center; + margin-bottom: var(--space-12); +} + +.tpos-badge { + display: inline-block; + background: var(--accent-glow); + color: var(--accent-primary); + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: var(--space-1) var(--space-3); + border-radius: var(--border-radius-base); + margin-bottom: var(--space-4); +} + +.tpos-section-title { + font-size: 2.25rem; + font-weight: 800; + line-height: 1.15; + margin-bottom: var(--space-4); + letter-spacing: -0.02em; +} + +.tpos-section-desc { + font-size: 1.0625rem; + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto; + line-height: 1.7; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 10. FEATURES GRID (6 feature cards) + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-feature-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: var(--space-6); + max-width: 1200px; + margin: 0 auto; +} + +@media (min-width: 640px) { + .tpos-feature-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .tpos-feature-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.tpos-feature-card { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--border-radius-xl); + padding: var(--space-8); + transition: all 0.25s ease; +} + +.tpos-feature-card:hover { + border-color: var(--border-default); + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +} + +.tpos-feature-icon { + width: 48px; + height: 48px; + border-radius: var(--border-radius-lg); + background: var(--accent-glow); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: var(--space-5); + color: var(--accent-primary); + font-size: 1.25rem; +} + +.tpos-feature-title { + font-size: 1.125rem; + font-weight: 700; + margin-bottom: var(--space-2); +} + +.tpos-feature-desc { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; + margin: 0; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 11. INDUSTRIES GRID (6 industry cards) + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-industry-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: var(--space-6); + max-width: 1200px; + margin: 0 auto; +} + +@media (min-width: 640px) { + .tpos-industry-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .tpos-industry-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.tpos-industry-card { + background: var(--bg-surface); + 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; +} + +.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-card:hover { + border-color: var(--border-default); + transform: translateY(-2px); + 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; + margin-bottom: var(--space-3); +} + +.tpos-industry-desc { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; + margin: 0 0 var(--space-4); +} + +.tpos-chips { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +.tpos-chip { + display: inline-flex; + align-items: center; + gap: var(--space-1); + background: var(--accent-glow); + color: var(--accent-light); + font-size: 0.75rem; + font-weight: 600; + padding: var(--space-1) var(--space-3); + border-radius: 999px; + border: 1px solid rgba(255, 92, 0, 0.2); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 12. ONBOARDING STEPS (3 steps) + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-steps { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: var(--space-8); + max-width: 1000px; + margin: 0 auto; +} + +@media (min-width: 768px) { + .tpos-steps { + grid-template-columns: repeat(3, 1fr); + } +} + +.tpos-step { + text-align: center; + padding: var(--space-8) var(--space-4); +} + +.tpos-step-num { + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--brand-gradient); + color: #fff; + font-size: 1.25rem; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto var(--space-6); + box-shadow: 0 0 30px var(--accent-glow-strong); +} + +.tpos-step-title { + font-size: 1.125rem; + font-weight: 700; + margin-bottom: var(--space-3); +} + +.tpos-step-desc { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; + margin: 0; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 13. PRICING SECTION + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-pricing-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: var(--space-6); + max-width: 1100px; + margin: 0 auto; + align-items: start; +} + +@media (min-width: 768px) { + .tpos-pricing-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.tpos-pricing-card { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--border-radius-2xl); + padding: var(--space-8); + position: relative; + transition: all 0.25s ease; +} + +.tpos-pricing-card:hover { + border-color: var(--border-default); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +} + +.tpos-pricing-card.featured { + border-color: var(--accent-primary); + box-shadow: 0 0 40px var(--accent-glow); + transform: scale(1.02); +} + +.tpos-pricing-badge { + font-size: 0.6875rem; + font-weight: 700; + color: var(--accent-primary); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: var(--space-2); +} + +.tpos-pricing-name { + font-size: 1.375rem; + font-weight: 800; + letter-spacing: 0.05em; + margin-bottom: var(--space-4); +} + +.tpos-pricing-price { + display: flex; + align-items: baseline; + gap: var(--space-1); + margin-bottom: var(--space-2); +} + +.tpos-pricing-amount { + font-size: 2.5rem; + font-weight: 800; + color: var(--accent-primary); +} + +.tpos-pricing-period { + font-size: 0.875rem; + color: var(--text-tertiary); +} + +.tpos-pricing-desc { + font-size: 0.8125rem; + color: var(--text-secondary); + margin-bottom: var(--space-6); + padding-bottom: var(--space-6); + border-bottom: 1px solid var(--border-subtle); +} + +.tpos-pricing-features { + list-style: none; + padding: 0; + margin: 0 0 var(--space-8); + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.tpos-pricing-feature { + display: flex; + align-items: flex-start; + gap: var(--space-3); + font-size: 0.875rem; + color: var(--text-secondary); +} + +.tpos-pricing-feature .check-icon { + color: var(--success); + font-size: 1rem; + flex-shrink: 0; + margin-top: 2px; +} + +/* Addons Grid */ +.tpos-addons { + margin-top: var(--space-16); + text-align: center; +} + +.tpos-addons-title { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: var(--space-8); + color: var(--text-secondary); +} + +.tpos-addons-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-4); + max-width: 800px; + margin: 0 auto; +} + +@media (min-width: 768px) { + .tpos-addons-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.tpos-addon-item { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--border-radius-lg); + padding: var(--space-5); + text-align: center; +} + +.tpos-addon-name { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--space-1); +} + +.tpos-addon-price { + font-size: 0.75rem; + color: var(--accent-light); + font-weight: 500; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 14. CTA SECTION + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-cta { + text-align: center; + padding: var(--space-24) var(--space-6); + position: relative; +} + +.tpos-cta::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 70%); + pointer-events: none; +} + +.tpos-cta-title { + font-size: 2.5rem; + font-weight: 800; + line-height: 1.15; + margin-bottom: var(--space-4); + letter-spacing: -0.02em; + position: relative; +} + +.tpos-cta-sub { + font-size: 1.0625rem; + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto var(--space-10); + line-height: 1.7; + position: relative; +} + +.tpos-cta-actions { + display: flex; + gap: var(--space-4); + justify-content: center; + flex-wrap: wrap; + margin-bottom: var(--space-6); + position: relative; +} + +.tpos-cta-trust { + font-size: 0.8125rem; + color: var(--text-tertiary); + position: relative; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 15. FOOTER + ═════════════════════════════════════════════════════════════════════════ */ +.tpos-footer { + border-top: 1px solid var(--border-subtle); + padding: var(--space-16) 0 var(--space-8); +} + +.tpos-footer-grid { + display: grid; + grid-template-columns: 2fr repeat(3, 1fr); + gap: var(--space-12); + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-6); +} + + + + +.tpos-footer-brand .tpos-logo { + margin-bottom: var(--space-4); + display: block; +} + +.tpos-footer-tagline { + font-size: 0.875rem; + color: var(--text-tertiary); + line-height: 1.6; +} + +.tpos-footer-col-title { + font-size: 0.8125rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-tertiary); + margin-bottom: var(--space-4); +} + +.tpos-footer-links { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.tpos-footer-link { + font-size: 0.875rem; + color: var(--text-secondary); + transition: color 0.2s ease; +} + +.tpos-footer-link:hover { + color: var(--text-primary); +} + +.tpos-footer-copy { + text-align: center; + padding: var(--space-8) var(--space-6) 0; + margin-top: var(--space-8); + border-top: 1px solid var(--border-subtle); + font-size: 0.8125rem; + color: var(--text-disabled); + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* ═════════════════════════════════════════════════════════════════════════ + 16. AUTHENTICATION COMPONENTS (kept from base) + ═════════════════════════════════════════════════════════════════════════ */ +.auth-container { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 64px); + padding: var(--space-6); +} + +.auth-card { + background-color: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--border-radius-xl); + padding: var(--space-12); + width: 100%; + max-width: 480px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4); +} + +.auth-title { + font-size: 2rem; + font-weight: 700; + margin-bottom: var(--space-2); + color: var(--text-primary); +} + +.auth-subtitle { + color: var(--text-secondary); + margin-bottom: var(--space-8); +} + +.auth-footer { + margin-top: var(--space-6); + text-align: center; + color: var(--text-secondary); + display: flex; + gap: var(--space-2); + align-items: center; + justify-content: center; +} + +/* Form Components */ +.form-group { + margin-bottom: var(--space-6); +} + +.form-group label { + display: block; + font-weight: 500; + margin-bottom: var(--space-2); + color: var(--text-primary); +} + +.form-input { + width: 100%; + padding: var(--space-3) var(--space-4); + border: 1px solid var(--border-default); + border-radius: var(--border-radius-base); + background-color: var(--bg-elevated); + color: var(--text-primary); + font-family: var(--font-body); + font-size: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.form-input:hover { + border-color: var(--border-strong); +} + +.form-input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.form-input:disabled { + background-color: var(--bg-interactive); + cursor: not-allowed; + opacity: 0.6; +} + +.form-hint { + display: block; + margin-top: var(--space-1); + font-size: 0.875rem; + color: var(--text-tertiary); +} + +.form-checkbox { + width: 1rem; + height: 1rem; + border: 1px solid var(--border-strong); + border-radius: 3px; + cursor: pointer; +} + +.checkbox-group { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.checkbox-label { + margin-bottom: 0; + font-weight: 400; + cursor: pointer; + user-select: none; +} + +.form-actions-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-6); +} + +/* General Buttons */ +.btn-primary, +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-6); + border-radius: var(--border-radius-base); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + border: none; + font-family: var(--font-body); +} + +.btn-primary { + background-color: var(--accent-primary); + color: #fff; +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--action-primary-bg-hover); +} + +.btn-secondary { + background-color: transparent; + color: var(--text-primary); + border: 1px solid var(--border-default); +} + +.btn-secondary:hover:not(:disabled) { + background-color: rgba(255, 255, 255, 0.05); +} + +.btn-primary:disabled, +.btn-secondary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-full { + width: 100%; +} + +.btn-group { + display: flex; + gap: var(--space-3); +} + +/* Links */ +.link-primary { + color: var(--accent-primary); + text-decoration: none; + font-weight: 500; + transition: opacity 0.2s ease; +} + +.link-primary:hover { + opacity: 0.8; + text-decoration: underline; +} + +.link-secondary { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + transition: color 0.2s ease; +} + +.link-secondary:hover { + color: var(--text-primary); + text-decoration: underline; +} + +/* Validation */ +.validation-message { + display: block; + margin-top: var(--space-1); + font-size: 0.875rem; + color: #ef4444; +} + +/* Alerts */ +.alert { + padding: var(--space-4); + border-radius: var(--border-radius-base); + margin-top: var(--space-6); + font-size: 0.875rem; +} + +.alert-success { + background-color: rgba(34, 197, 94, 0.15); + color: #4ade80; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.alert-error { + background-color: rgba(239, 68, 68, 0.15); + color: #fca5a5; + border: 1px solid rgba(239, 68, 68, 0.3); +} + +/* Spinners */ +.spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 4px solid var(--border-subtle); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.spinner-small { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: currentColor; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Loading & Success States */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-12); +} + +.success-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-8); +} + +.checkmark { + color: var(--success); + stroke-width: 3; +} + +/* Profile Page */ +.profile-container { + max-width: 800px; + margin: 0 auto; + padding: var(--space-8) var(--space-6); +} + +.profile-header { + margin-bottom: var(--space-12); +} + +.profile-title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: var(--space-2); +} + +.profile-subtitle { + color: var(--text-secondary); + font-size: 1.125rem; +} + +.profile-content { + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.profile-section { + background-color: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--border-radius-xl); + padding: var(--space-8); +} + +.section-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: var(--space-6); + color: var(--text-primary); +} + +.profile-actions { + margin-top: var(--space-8); + padding-top: var(--space-8); + border-top: 1px solid var(--border-subtle); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 17. UTILITY CLASSES + ═════════════════════════════════════════════════════════════════════════ */ +.text-center { + text-align: center; +} + +.text-secondary { + color: var(--text-secondary); +} + +.text-lg { + font-size: 1.125rem; +} + +.mt-4 { + margin-top: var(--space-4); +} + +.mt-6 { + margin-top: var(--space-6); +} + +.mb-6 { + margin-bottom: var(--space-6); +} + +.mr-2 { + margin-right: var(--space-2); +} + +/* ═════════════════════════════════════════════════════════════════════════ + 18. RESPONSIVE — Hamburger Menu + Mobile Drawer + ═════════════════════════════════════════════════════════════════════════ */ + +/* Hamburger button — hidden on desktop, visible ≤ 768px */ +.tpos-hamburger { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 8px; + z-index: 1100; +} + +.tpos-hamburger span { + display: block; + width: 24px; + height: 2px; + background: var(--text-primary); + border-radius: 2px; + transition: all 0.3s ease; +} + +/* Hamburger → X animation */ +.tpos-hamburger.active span:nth-child(1) { + transform: rotate(45deg) translate(5px, 5px); +} + +.tpos-hamburger.active span:nth-child(2) { + opacity: 0; +} + +.tpos-hamburger.active span:nth-child(3) { + transform: rotate(-45deg) translate(5px, -5px); +} + +/* Mobile overlay (backdrop) */ +.tpos-mobile-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 1050; + backdrop-filter: blur(4px); + animation: fadeIn 0.2s ease; +} + +/* Mobile drawer */ +.tpos-mobile-drawer { + position: fixed; + top: 0; + right: 0; + width: 280px; + max-width: 85vw; + height: 100vh; + background: var(--bg-elevated); + border-left: 1px solid var(--border-subtle); + z-index: 1100; + padding: var(--space-16) var(--space-6) var(--space-8); + display: flex; + flex-direction: column; + gap: var(--space-2); + animation: slideIn 0.25s ease; + overflow-y: auto; +} + +.tpos-mobile-link { + display: block; + padding: var(--space-4) var(--space-4); + color: var(--text-secondary); + font-size: 1rem; + font-weight: 500; + border-radius: var(--border-radius-md); + transition: all 0.2s ease; +} + +.tpos-mobile-link:hover { + color: var(--text-primary); + background: var(--bg-surface); +} + +.tpos-mobile-actions { + margin-top: auto; + padding-top: var(--space-6); + border-top: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* ═════════════════════════════════════════════════════════════════════════ + 19. RESPONSIVE — Tablet (≤ 768px) + ═════════════════════════════════════════════════════════════════════════ */ +@media (max-width: 768px) { + + /* Show hamburger, hide desktop nav */ + .tpos-hamburger { + display: flex; + } + + .tpos-nav-links { + display: none; + } + + /* Typography scaling */ + .hero-headline { + font-size: clamp(2rem, 6vw, 3rem); + } + + .hero-subtext { + font-size: 1rem; + } + + .hero-section { + padding: var(--space-12) var(--space-4) var(--space-10); + } + + .hero-mockup { + height: 260px; + margin-top: var(--space-10); + } + + /* Section titles */ + .tpos-section-title { + font-size: clamp(1.5rem, 5vw, 2.25rem); + } + + .tpos-cta-title { + font-size: clamp(1.5rem, 5vw, 2.25rem); + } + + /* Trust stats — stack vertically */ + .trust-stats { + flex-direction: column; + gap: var(--space-4); + } + + /* Hero buttons — stack on smaller screens */ + .hero-actions { + flex-direction: column; + align-items: center; + } + + .hero-actions .btn-accent-lg, + .hero-actions .btn-outline-lg { + width: 100%; + max-width: 320px; + justify-content: center; + } + + /* CTA actions — stack */ + .tpos-cta-actions { + flex-direction: column; + align-items: center; + } + + .tpos-cta-actions .btn-accent-lg, + .tpos-cta-actions .btn-outline-lg { + width: 100%; + max-width: 320px; + justify-content: center; + } + + /* Features grid — 2 columns on tablet */ + .tpos-features-grid { + grid-template-columns: repeat(2, 1fr); + } + + /* Industries grid — 2 columns */ + .tpos-industries-grid { + grid-template-columns: repeat(2, 1fr); + } + + /* Footer — single column */ + .tpos-footer-grid { + grid-template-columns: 1fr; + gap: var(--space-6); + } + + /* Footer bottom */ + .tpos-footer-bottom { + flex-direction: column; + gap: var(--space-3); + text-align: center; + } + + /* Addons — 2 columns */ + .tpos-addons-grid { + grid-template-columns: repeat(2, 1fr); + } + + /* Pricing featured card — no scale */ + .tpos-pricing-card.featured { + transform: none; + } +} + +/* ═════════════════════════════════════════════════════════════════════════ + 20. RESPONSIVE — Mobile (≤ 480px) + ═════════════════════════════════════════════════════════════════════════ */ +@media (max-width: 480px) { + + /* Hero */ + .hero-headline { + font-size: 1.875rem; + } + + .hero-subtext { + font-size: 0.9375rem; + } + + .hero-section { + padding: var(--space-8) var(--space-4) var(--space-8); + } + + .hero-mockup { + height: 200px; + } + + /* Hero buttons — stack vertically */ + .hero-actions { + flex-direction: column; + align-items: center; + } + + .hero-actions .btn-accent-lg, + .hero-actions .btn-outline-lg { + width: 100%; + justify-content: center; + } + + /* Section titles */ + .tpos-section-title { + font-size: 1.5rem; + } + + .tpos-section-desc { + font-size: 0.875rem; + } + + /* Trust stats — stack vertically */ + .trust-stats { + flex-direction: column; + gap: var(--space-4); + } + + .trust-stat { + font-size: 0.9375rem; + } + + /* Features grid — 1 column */ + .tpos-features-grid { + grid-template-columns: 1fr; + } + + /* Industries grid — 1 column */ + .tpos-industries-grid { + grid-template-columns: 1fr; + } + + /* Pricing — 1 column */ + .tpos-pricing-grid { + grid-template-columns: 1fr; + } + + /* Addons — 2 columns */ + .tpos-addons-grid { + grid-template-columns: repeat(2, 1fr); + } + + /* CTA actions — stack vertically */ + .tpos-cta-actions { + flex-direction: column; + align-items: center; + } + + .tpos-cta-actions .btn-accent-lg, + .tpos-cta-actions .btn-outline-lg { + width: 100%; + justify-content: center; + } + + .tpos-cta-title { + font-size: 1.5rem; + } + + /* Footer — 1 column */ + .tpos-footer-grid { + grid-template-columns: 1fr; + gap: var(--space-6); + } + + /* Footer bottom */ + .tpos-footer-bottom { + flex-direction: column; + gap: var(--space-3); + text-align: center; + } + + /* Section padding reduction */ + .tpos-section, + .tpos-cta { + padding-left: var(--space-4); + padding-right: var(--space-4); + } +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/favicon.png b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/favicon.png new file mode 100644 index 00000000..8422b596 Binary files /dev/null and b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/favicon.png differ diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/icon-192.png b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/icon-192.png new file mode 100644 index 00000000..166f56da Binary files /dev/null and b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/icon-192.png differ diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/logo.png b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/logo.png new file mode 100644 index 00000000..26e4059a Binary files /dev/null and b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/logo.png differ diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/store.png b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/store.png new file mode 100644 index 00000000..880f5f8e Binary files /dev/null and b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/images/eggymon/store.png differ diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html new file mode 100644 index 00000000..54bd5d9f --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html @@ -0,0 +1,77 @@ + + + + + + + aPOS - Smart POS System for Modern Business + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + +

Loading + aPOS...

+
+ + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json new file mode 100644 index 00000000..685278db --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json @@ -0,0 +1,170 @@ +{ + "AppName": "aPOS", + "Nav_Features": "Features", + "Nav_Industries": "Industries", + "Nav_Pricing": "Pricing", + "Nav_Contact": "Contact", + "Nav_Login": "Login", + "Nav_FreeTrial": "Start Free Trial", + "HeroBadge": "🚀 Powered by AI", + "HeroHeadline": "The Smart POS System
for Modern Business", + "HeroSubtext": "All-in-one point-of-sale solution with AI-powered insights, real-time analytics, and seamless multi-store management. Built for speed, designed for growth.", + "HeroCTA_Primary": "Start Free Trial", + "HeroCTA_Secondary": "Watch Demo", + "HeroMockup_Alt": "aPOS Dashboard Preview", + "Trust_Label": "Trusted by businesses across Southeast Asia", + "Trust_Stat1": "5,000+ Active Stores", + "Trust_Stat2": "10M+ Transactions/month", + "Trust_Stat3": "99.9% Uptime", + "Features_Badge": "Features", + "Features_Title": "Everything You Need to Run Your Business", + "Features_Desc": "From point-of-sale to back-office management, aPOS gives you the tools to streamline operations and grow revenue.", + "Feature_POS_Title": "Smart POS", + "Feature_POS_Desc": "Lightning-fast checkout with barcode scanning, split payments, and offline mode. Process orders in seconds.", + "Feature_POS_Icon": "💳", + "Feature_Loyalty_Title": "Loyalty & CRM", + "Feature_Loyalty_Desc": "Build customer loyalty with points, rewards, and personalized campaigns. Know your customers better.", + "Feature_Loyalty_Icon": "❤️", + "Feature_Reports_Title": "Real-time Reports", + "Feature_Reports_Desc": "AI-powered analytics and dashboards. Track sales, staff performance, and inventory in real-time.", + "Feature_Reports_Icon": "📊", + "Feature_Staff_Title": "Staff Management", + "Feature_Staff_Desc": "Schedule shifts, track attendance, manage permissions, and monitor performance across all locations.", + "Feature_Staff_Icon": "👥", + "Feature_Inventory_Title": "Inventory Control", + "Feature_Inventory_Desc": "Automatic stock alerts, supplier management, and multi-warehouse tracking. Never run out of stock.", + "Feature_Inventory_Icon": "📦", + "Feature_Payments_Title": "Multi-Payment", + "Feature_Payments_Desc": "Accept cash, cards, e-wallets, QR codes, and bank transfers. All payment methods in one system.", + "Feature_Payments_Icon": "💰", + "Industries_Badge": "Industries", + "Industries_Title": "Built for Every Business Type", + "Industries_Desc": "Tailored solutions for different industries with specialized features and workflows.", + "Industry_Restaurant_Title": "Restaurant & Café", + "Industry_Restaurant_Desc": "Table management, kitchen display, split bills, and tip handling for dine-in and takeaway.", + "Industry_Restaurant_Chips": "AI Menu Suggestions, Smart Table Management", + "Industry_Bar_Title": "Bar & Nightclub", + "Industry_Bar_Desc": "Tab management, happy hour pricing, age verification, and high-speed drink ordering.", + "Industry_Bar_Chips": "AI Inventory Forecast, Tab Analytics", + "Industry_Karaoke_Title": "Karaoke & Entertainment", + "Industry_Karaoke_Desc": "Room booking, hourly billing, food & drink combo packages, and membership integration.", + "Industry_Karaoke_Chips": "AI Dynamic Pricing, Room Optimization", + "Industry_Coffee_Title": "Coffee & Bakery", + "Industry_Coffee_Desc": "Quick-serve mode, recipe management, customization modifiers, and loyalty stamp cards.", + "Industry_Coffee_Chips": "AI Order Prediction, Peak Scheduling", + "Industry_Spa_Title": "Spa & Beauty", + "Industry_Spa_Desc": "Appointment booking, therapist scheduling, service packages, and commission tracking.", + "Industry_Spa_Chips": "AI Scheduling, Customer Insights", + "Industry_Retail_Title": "Retail & Fashion", + "Industry_Retail_Desc": "Barcode management, size/color variants, multi-store inventory sync, and customer wishlists.", + "Industry_Retail_Chips": "AI Demand Forecast, Smart Reorder", + "Steps_Badge": "Get Started", + "Steps_Title": "Up and Running in 3 Simple Steps", + "Steps_Desc": "No complex setup, no lengthy contracts. Start accepting orders today.", + "Step1_Title": "Sign Up & Configure", + "Step1_Desc": "Create your account, add your menu or products, and configure your business settings in minutes.", + "Step2_Title": "Install & Connect", + "Step2_Desc": "Download aPOS on any device – tablet, phone, or desktop. Connect your printers and payment terminals.", + "Step3_Title": "Start Selling", + "Step3_Desc": "Go live! Process your first transaction and watch real-time analytics flow into your dashboard.", + "Pricing_Badge": "Pricing", + "Pricing_Title": "Simple, Transparent Pricing", + "Pricing_Desc": "No hidden fees. Choose the plan that fits your business and scale as you grow.", + "Plan_Starter_Badge": "STARTER", + "Plan_Starter_Name": "STARTER", + "Plan_Starter_Price": "Free", + "Plan_Starter_Period": "", + "Plan_Starter_Desc": "Perfect for small businesses just getting started", + "Plan_Starter_Feature1": "1 Store, 1 Terminal", + "Plan_Starter_Feature2": "Basic POS & Checkout", + "Plan_Starter_Feature3": "Daily Sales Reports", + "Plan_Starter_Feature4": "Email Support", + "Plan_Starter_CTA": "Get Started Free", + "Plan_Pro_Badge": "MOST POPULAR", + "Plan_Pro_Name": "PROFESSIONAL", + "Plan_Pro_Price": "499K", + "Plan_Pro_Period": "/month", + "Plan_Pro_Desc": "For growing businesses that need more power", + "Plan_Pro_Feature1": "Up to 5 Stores, Unlimited Terminals", + "Plan_Pro_Feature2": "Full POS + Loyalty + CRM", + "Plan_Pro_Feature3": "Advanced Analytics & Reports", + "Plan_Pro_Feature4": "Staff Management", + "Plan_Pro_Feature5": "Priority Support 24/7", + "Plan_Pro_CTA": "Start Free Trial", + "Plan_Enterprise_Badge": "ENTERPRISE", + "Plan_Enterprise_Name": "ENTERPRISE", + "Plan_Enterprise_Price": "Custom", + "Plan_Enterprise_Period": "", + "Plan_Enterprise_Desc": "For chains and franchises requiring enterprise-grade features", + "Plan_Enterprise_Feature1": "Unlimited Stores & Terminals", + "Plan_Enterprise_Feature2": "White-label Solution", + "Plan_Enterprise_Feature3": "Custom Integrations & API", + "Plan_Enterprise_Feature4": "Dedicated Account Manager", + "Plan_Enterprise_Feature5": "SLA & On-site Training", + "Plan_Enterprise_CTA": "Contact Sales", + "Addons_Title": "Add-on Modules", + "Addon_KDS_Name": "Kitchen Display", + "Addon_KDS_Price": "99K/mo", + "Addon_Delivery_Name": "Delivery Integration", + "Addon_Delivery_Price": "149K/mo", + "Addon_Accounting_Name": "Accounting Sync", + "Addon_Accounting_Price": "99K/mo", + "Addon_EInvoice_Name": "E-Invoice", + "Addon_EInvoice_Price": "79K/mo", + "Addon_Marketing_Name": "Marketing Hub", + "Addon_Marketing_Price": "129K/mo", + "Addon_Reservation_Name": "Reservations", + "Addon_Reservation_Price": "99K/mo", + "CTA_Title": "Ready to Transform Your Business?", + "CTA_Subtitle": "Join 5,000+ businesses already using aPOS to grow revenue, reduce costs, and delight their customers.", + "CTA_Primary": "Start Free Trial", + "CTA_Secondary": "Talk to Sales", + "CTA_Trust": "No credit card required · Free for 14 days · Cancel anytime", + "Footer_Tagline": "The smart POS system for modern businesses across Southeast Asia.", + "Footer_Col1_Title": "Product", + "Footer_Col1_Link1": "Features", + "Footer_Col1_Link2": "Pricing", + "Footer_Col1_Link3": "Integrations", + "Footer_Col1_Link4": "Changelog", + "Footer_Col2_Title": "Resources", + "Footer_Col2_Link1": "Documentation", + "Footer_Col2_Link2": "API Reference", + "Footer_Col2_Link3": "Blog", + "Footer_Col2_Link4": "Help Center", + "Footer_Col3_Title": "Company", + "Footer_Col3_Link1": "About Us", + "Footer_Col3_Link2": "Careers", + "Footer_Col3_Link3": "Contact", + "Footer_Col3_Link4": "Partners", + "Footer_Copyright": "© 2025 aPOS by GoodGo. All rights reserved.", + "Auth_Login_Title": "Sign In", + "Auth_Login_Subtitle": "Welcome back! Please sign in to your account.", + "Auth_Login_Email": "Email", + "Auth_Login_Password": "Password", + "Auth_Login_RememberMe": "Remember me", + "Auth_Login_ForgotPassword": "Forgot password?", + "Auth_Login_Submit": "Sign In", + "Auth_Login_NoAccount": "Don't have an account?", + "Auth_Login_Register": "Create one", + "Auth_Register_Title": "Create Account", + "Auth_Register_Subtitle": "Start your free trial today. No credit card required.", + "Auth_Register_FullName": "Full Name", + "Auth_Register_Email": "Email", + "Auth_Register_Password": "Password", + "Auth_Register_ConfirmPassword": "Confirm Password", + "Auth_Register_BusinessName": "Business Name", + "Auth_Register_AgreeTerms": "I agree to the Terms of Service and Privacy Policy", + "Auth_Register_Submit": "Create Account", + "Auth_Register_HasAccount": "Already have an account?", + "Auth_Register_Login": "Sign in", + "Auth_ForgotPwd_Title": "Reset Password", + "Auth_ForgotPwd_Subtitle": "Enter your email and we'll send you a reset link.", + "Auth_ForgotPwd_Email": "Email", + "Auth_ForgotPwd_Submit": "Send Reset Link", + "Auth_ForgotPwd_BackToLogin": "Back to Sign In", + "Validation_Required": "This field is required", + "Validation_Email": "Please enter a valid email address", + "Validation_PasswordMin": "Password must be at least 8 characters", + "Validation_PasswordMatch": "Passwords do not match", + "Validation_AgreeTerms": "You must agree to the terms" +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json new file mode 100644 index 00000000..143b3aee --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json @@ -0,0 +1,170 @@ +{ + "AppName": "aPOS", + "Nav_Features": "Tính năng", + "Nav_Industries": "Ngành hàng", + "Nav_Pricing": "Bảng giá", + "Nav_Contact": "Liên hệ", + "Nav_Login": "Đăng nhập", + "Nav_FreeTrial": "Dùng thử miễn phí", + "HeroBadge": "🚀 Trang bị AI", + "HeroHeadline": "Hệ thống POS Thông minh
cho Doanh nghiệp Hiện đại", + "HeroSubtext": "Giải pháp bán hàng tất-cả-trong-một với phân tích AI, báo cáo thời gian thực và quản lý đa cửa hàng. Xây dựng cho tốc độ, thiết kế cho tăng trưởng.", + "HeroCTA_Primary": "Dùng thử miễn phí", + "HeroCTA_Secondary": "Xem Demo", + "HeroMockup_Alt": "Giao diện bảng điều khiển aPOS", + "Trust_Label": "Được tin dùng bởi các doanh nghiệp tại Đông Nam Á", + "Trust_Stat1": "5,000+ Cửa hàng hoạt động", + "Trust_Stat2": "10M+ Giao dịch/tháng", + "Trust_Stat3": "99.9% Thời gian hoạt động", + "Features_Badge": "Tính năng", + "Features_Title": "Mọi thứ bạn cần để vận hành doanh nghiệp", + "Features_Desc": "Từ quầy bán hàng đến quản lý back-office, aPOS cung cấp công cụ giúp tối ưu vận hành và tăng doanh thu.", + "Feature_POS_Title": "POS Thông minh", + "Feature_POS_Desc": "Thanh toán siêu nhanh với quét mã vạch, chia hóa đơn, và chế độ offline. Xử lý đơn hàng trong tích tắc.", + "Feature_POS_Icon": "💳", + "Feature_Loyalty_Title": "Khách hàng & CRM", + "Feature_Loyalty_Desc": "Xây dựng lòng trung thành với tích điểm, ưu đãi và chiến dịch cá nhân hóa. Hiểu khách hàng hơn.", + "Feature_Loyalty_Icon": "❤️", + "Feature_Reports_Title": "Báo cáo Thời gian thực", + "Feature_Reports_Desc": "Phân tích AI và bảng điều khiển trực quan. Theo dõi doanh số, hiệu suất nhân viên và tồn kho theo thời gian thực.", + "Feature_Reports_Icon": "📊", + "Feature_Staff_Title": "Quản lý Nhân viên", + "Feature_Staff_Desc": "Lên lịch ca, chấm công, phân quyền và giám sát hiệu suất trên tất cả chi nhánh.", + "Feature_Staff_Icon": "👥", + "Feature_Inventory_Title": "Quản lý Kho hàng", + "Feature_Inventory_Desc": "Cảnh báo tồn kho tự động, quản lý nhà cung cấp và theo dõi đa kho. Không bao giờ hết hàng.", + "Feature_Inventory_Icon": "📦", + "Feature_Payments_Title": "Đa Phương thức Thanh toán", + "Feature_Payments_Desc": "Chấp nhận tiền mặt, thẻ, ví điện tử, mã QR và chuyển khoản. Tất cả trong một hệ thống.", + "Feature_Payments_Icon": "💰", + "Industries_Badge": "Ngành hàng", + "Industries_Title": "Thiết kế cho Mọi Ngành Kinh doanh", + "Industries_Desc": "Giải pháp tùy chỉnh cho từng ngành hàng với tính năng và quy trình chuyên biệt.", + "Industry_Restaurant_Title": "Nhà hàng & Quán ăn", + "Industry_Restaurant_Desc": "Quản lý bàn, màn hình bếp, chia hóa đơn và tip cho ăn tại chỗ và mang về.", + "Industry_Restaurant_Chips": "AI Gợi ý Menu, Quản lý Bàn Thông minh", + "Industry_Bar_Title": "Bar & Nightclub", + "Industry_Bar_Desc": "Quản lý tab, giá happy hour, xác minh tuổi và gọi đồ uống tốc độ cao.", + "Industry_Bar_Chips": "AI Dự đoán Tồn kho, Phân tích Tab", + "Industry_Karaoke_Title": "Karaoke & Giải trí", + "Industry_Karaoke_Desc": "Đặt phòng, tính tiền theo giờ, combo đồ ăn uống và tích hợp thành viên.", + "Industry_Karaoke_Chips": "AI Giá Động, Tối ưu Phòng", + "Industry_Coffee_Title": "Cà phê & Bánh", + "Industry_Coffee_Desc": "Chế độ bán nhanh, quản lý công thức, tùy chỉnh topping và thẻ tích điểm.", + "Industry_Coffee_Chips": "AI Dự đoán Đơn, Lịch Giờ Cao điểm", + "Industry_Spa_Title": "Spa & Làm đẹp", + "Industry_Spa_Desc": "Đặt lịch hẹn, xếp lịch kỹ thuật viên, gói dịch vụ và theo dõi hoa hồng.", + "Industry_Spa_Chips": "AI Xếp lịch, Phân tích Khách hàng", + "Industry_Retail_Title": "Bán lẻ & Thời trang", + "Industry_Retail_Desc": "Quản lý mã vạch, biến thể size/màu, đồng bộ tồn kho đa cửa hàng và wishlist.", + "Industry_Retail_Chips": "AI Dự báo Nhu cầu, Đặt hàng Thông minh", + "Steps_Badge": "Bắt đầu", + "Steps_Title": "Hoạt động chỉ trong 3 Bước Đơn giản", + "Steps_Desc": "Không cần cài đặt phức tạp, không hợp đồng dài hạn. Bắt đầu nhận đơn ngay hôm nay.", + "Step1_Title": "Đăng ký & Cấu hình", + "Step1_Desc": "Tạo tài khoản, thêm menu hoặc sản phẩm, và cấu hình cài đặt doanh nghiệp trong vài phút.", + "Step2_Title": "Cài đặt & Kết nối", + "Step2_Desc": "Tải aPOS trên mọi thiết bị — tablet, điện thoại hoặc máy tính. Kết nối máy in và terminal thanh toán.", + "Step3_Title": "Bắt đầu Bán hàng", + "Step3_Desc": "Sẵn sàng! Xử lý giao dịch đầu tiên và xem phân tích thời gian thực trên bảng điều khiển.", + "Pricing_Badge": "Bảng giá", + "Pricing_Title": "Bảng giá Minh bạch, Đơn giản", + "Pricing_Desc": "Không phí ẩn. Chọn gói phù hợp với doanh nghiệp và mở rộng khi phát triển.", + "Plan_Starter_Badge": "KHỞI ĐẦU", + "Plan_Starter_Name": "KHỞI ĐẦU", + "Plan_Starter_Price": "Miễn phí", + "Plan_Starter_Period": "", + "Plan_Starter_Desc": "Hoàn hảo cho doanh nghiệp nhỏ mới bắt đầu", + "Plan_Starter_Feature1": "1 Cửa hàng, 1 Máy POS", + "Plan_Starter_Feature2": "POS & Thanh toán cơ bản", + "Plan_Starter_Feature3": "Báo cáo Doanh số hàng ngày", + "Plan_Starter_Feature4": "Hỗ trợ qua Email", + "Plan_Starter_CTA": "Bắt đầu Miễn phí", + "Plan_Pro_Badge": "PHỔ BIẾN NHẤT", + "Plan_Pro_Name": "CHUYÊN NGHIỆP", + "Plan_Pro_Price": "499K", + "Plan_Pro_Period": "/tháng", + "Plan_Pro_Desc": "Cho doanh nghiệp đang phát triển cần thêm sức mạnh", + "Plan_Pro_Feature1": "Tối đa 5 Cửa hàng, Không giới hạn Máy POS", + "Plan_Pro_Feature2": "Đầy đủ POS + Loyalty + CRM", + "Plan_Pro_Feature3": "Phân tích & Báo cáo Nâng cao", + "Plan_Pro_Feature4": "Quản lý Nhân viên", + "Plan_Pro_Feature5": "Hỗ trợ Ưu tiên 24/7", + "Plan_Pro_CTA": "Dùng thử Miễn phí", + "Plan_Enterprise_Badge": "DOANH NGHIỆP", + "Plan_Enterprise_Name": "DOANH NGHIỆP", + "Plan_Enterprise_Price": "Liên hệ", + "Plan_Enterprise_Period": "", + "Plan_Enterprise_Desc": "Cho chuỗi và nhượng quyền cần tính năng cấp doanh nghiệp", + "Plan_Enterprise_Feature1": "Không giới hạn Cửa hàng & Máy POS", + "Plan_Enterprise_Feature2": "Giải pháp White-label", + "Plan_Enterprise_Feature3": "Tích hợp Tùy chỉnh & API", + "Plan_Enterprise_Feature4": "Quản lý Tài khoản Chuyên trách", + "Plan_Enterprise_Feature5": "SLA & Đào tạo Tại chỗ", + "Plan_Enterprise_CTA": "Liên hệ Tư vấn", + "Addons_Title": "Module Bổ sung", + "Addon_KDS_Name": "Màn hình Bếp", + "Addon_KDS_Price": "99K/tháng", + "Addon_Delivery_Name": "Tích hợp Giao hàng", + "Addon_Delivery_Price": "149K/tháng", + "Addon_Accounting_Name": "Đồng bộ Kế toán", + "Addon_Accounting_Price": "99K/tháng", + "Addon_EInvoice_Name": "Hóa đơn Điện tử", + "Addon_EInvoice_Price": "79K/tháng", + "Addon_Marketing_Name": "Marketing Hub", + "Addon_Marketing_Price": "129K/tháng", + "Addon_Reservation_Name": "Đặt bàn", + "Addon_Reservation_Price": "99K/tháng", + "CTA_Title": "Sẵn sàng Chuyển đổi Doanh nghiệp?", + "CTA_Subtitle": "Tham gia cùng 5,000+ doanh nghiệp đang sử dụng aPOS để tăng doanh thu, giảm chi phí và làm hài lòng khách hàng.", + "CTA_Primary": "Dùng thử Miễn phí", + "CTA_Secondary": "Nói chuyện với Tư vấn", + "CTA_Trust": "Không cần thẻ tín dụng · Miễn phí 14 ngày · Hủy bất cứ lúc nào", + "Footer_Tagline": "Hệ thống POS thông minh cho doanh nghiệp hiện đại tại Đông Nam Á.", + "Footer_Col1_Title": "Sản phẩm", + "Footer_Col1_Link1": "Tính năng", + "Footer_Col1_Link2": "Bảng giá", + "Footer_Col1_Link3": "Tích hợp", + "Footer_Col1_Link4": "Nhật ký Thay đổi", + "Footer_Col2_Title": "Tài nguyên", + "Footer_Col2_Link1": "Tài liệu Hướng dẫn", + "Footer_Col2_Link2": "Tài liệu API", + "Footer_Col2_Link3": "Blog", + "Footer_Col2_Link4": "Trung tâm Trợ giúp", + "Footer_Col3_Title": "Công ty", + "Footer_Col3_Link1": "Về Chúng tôi", + "Footer_Col3_Link2": "Tuyển dụng", + "Footer_Col3_Link3": "Liên hệ", + "Footer_Col3_Link4": "Đối tác", + "Footer_Copyright": "© 2025 aPOS bởi GoodGo. Bảo lưu mọi quyền.", + "Auth_Login_Title": "Đăng nhập", + "Auth_Login_Subtitle": "Chào mừng trở lại! Vui lòng đăng nhập vào tài khoản.", + "Auth_Login_Email": "Email", + "Auth_Login_Password": "Mật khẩu", + "Auth_Login_RememberMe": "Ghi nhớ đăng nhập", + "Auth_Login_ForgotPassword": "Quên mật khẩu?", + "Auth_Login_Submit": "Đăng nhập", + "Auth_Login_NoAccount": "Chưa có tài khoản?", + "Auth_Login_Register": "Tạo tài khoản", + "Auth_Register_Title": "Tạo Tài khoản", + "Auth_Register_Subtitle": "Bắt đầu dùng thử miễn phí. Không cần thẻ tín dụng.", + "Auth_Register_FullName": "Họ và Tên", + "Auth_Register_Email": "Email", + "Auth_Register_Password": "Mật khẩu", + "Auth_Register_ConfirmPassword": "Xác nhận Mật khẩu", + "Auth_Register_BusinessName": "Tên Doanh nghiệp", + "Auth_Register_AgreeTerms": "Tôi đồng ý với Điều khoản Dịch vụ và Chính sách Bảo mật", + "Auth_Register_Submit": "Tạo Tài khoản", + "Auth_Register_HasAccount": "Đã có tài khoản?", + "Auth_Register_Login": "Đăng nhập", + "Auth_ForgotPwd_Title": "Đặt lại Mật khẩu", + "Auth_ForgotPwd_Subtitle": "Nhập email của bạn để nhận liên kết đặt lại mật khẩu.", + "Auth_ForgotPwd_Email": "Email", + "Auth_ForgotPwd_Submit": "Gửi liên kết Đặt lại", + "Auth_ForgotPwd_BackToLogin": "Quay lại Đăng nhập", + "Validation_Required": "Trường này là bắt buộc", + "Validation_Email": "Vui lòng nhập địa chỉ email hợp lệ", + "Validation_PasswordMin": "Mật khẩu phải có ít nhất 8 ký tự", + "Validation_PasswordMatch": "Mật khẩu không khớp", + "Validation_AgreeTerms": "Bạn phải đồng ý với các điều khoản" +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/sample-data/weather.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/sample-data/weather.json new file mode 100644 index 00000000..b7459733 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/sample-data/weather.json @@ -0,0 +1,27 @@ +[ + { + "date": "2022-01-06", + "temperatureC": 1, + "summary": "Freezing" + }, + { + "date": "2022-01-07", + "temperatureC": 14, + "summary": "Bracing" + }, + { + "date": "2022-01-08", + "temperatureC": -13, + "summary": "Freezing" + }, + { + "date": "2022-01-09", + "temperatureC": -16, + "summary": "Balmy" + }, + { + "date": "2022-01-10", + "temperatureC": -2, + "summary": "Chilly" + } +] diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs new file mode 100644 index 00000000..c4d43548 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs @@ -0,0 +1,123 @@ +/// +/// EN: ASP.NET Core BFF (Backend for Frontend) with YARP Reverse Proxy. +/// VI: ASP.NET Core BFF (Backend for Frontend) với YARP Reverse Proxy. +/// + +using Microsoft.AspNetCore.Rewrite; + +var builder = WebApplication.CreateBuilder(args); + +// ═══════════════════════════════════════════════════════════════════════════════ +// EN: Add services to the container +// VI: Thêm các services vào container +// ═══════════════════════════════════════════════════════════════════════════════ + +// EN: Load YARP configuration from yarp.json +// VI: Load cấu hình YARP từ yarp.json +builder.Configuration.AddJsonFile("yarp.json", optional: false, reloadOnChange: true); + +// EN: Add YARP Reverse Proxy +// VI: Thêm YARP Reverse Proxy +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + +// EN: Add OpenAPI/Swagger support +// VI: Thêm hỗ trợ OpenAPI/Swagger +builder.Services.AddOpenApi(); + +// EN: Add CORS for Blazor WebAssembly client +// VI: Thêm CORS cho Blazor WebAssembly client +builder.Services.AddCors(options => +{ + options.AddPolicy("BlazorClient", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +// EN: Add health checks +// VI: Thêm health checks +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// ═══════════════════════════════════════════════════════════════════════════════ +// EN: Configure the HTTP request pipeline +// VI: Cấu hình HTTP request pipeline +// ═══════════════════════════════════════════════════════════════════════════════ + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.UseDeveloperExceptionPage(); +} + +app.UseHttpsRedirection(); + +// EN: Enable CORS +// VI: Kích hoạt CORS +// EN: Enable CORS +// VI: Kích hoạt CORS +app.UseCors("BlazorClient"); + +// EN: Rewrite localized framework/content requests to root +// VI: Viết lại các yêu cầu framework/content từ đường dẫn ngôn ngữ về root +var rewriteOptions = new RewriteOptions() + .AddRewrite(@"^(en-US|vi-VN)/(_framework|_content)/(.*)", "$2/$3", skipRemainingRules: true); +app.UseRewriter(rewriteOptions); + +// EN: Serve static files with fingerprinting support (.NET 10+) +// VI: Phục vụ static files với hỗ trợ fingerprinting (.NET 10+) +app.MapStaticAssets(); + +// EN: Map health check endpoint +// VI: Map endpoint health check +app.MapHealthChecks("/health"); + +// EN: Map YARP Reverse Proxy routes to microservices +// VI: Map các routes YARP Reverse Proxy đến microservices +app.MapReverseProxy(); + +// EN: Localization Support - Serve index.html with dynamic base tag for specific cultures +// VI: Hỗ trợ đa ngôn ngữ - Phục vụ index.html với base tag động cho các ngôn ngữ cụ thể +var supportedCultures = new[] { "en-US", "vi-VN" }; +var localizationOptions = new RequestLocalizationOptions() + .SetDefaultCulture("en-US") + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures); + +app.UseRequestLocalization(localizationOptions); + +// Handle mapped culture routes (e.g. /en-US/home, /vi-VN/solutions) +app.Map("{culture:regex(^(en-US|vi-VN)$)}/{**slug}", async (string culture, HttpContext context, IWebHostEnvironment env) => +{ + // Try to find index.html + var fileInfo = env.WebRootFileProvider.GetFileInfo("index.html"); + if (!fileInfo.Exists) + { + // In Development with Hosted Blazor, index.html might not be in Server's wwwroot strictly directly depending on setup, + // but typically it is served via StaticFiles/BlazorFrameworkFiles. + // If we can't find it easily via IWebHostEnvironment in Dev, we might fail. + // However, for this task let's assume standard structure or handle gracefully. + return Results.NotFound("index.html not found in wwwroot. Ensure the Client project is built."); + } + + using var stream = fileInfo.CreateReadStream(); + using var reader = new StreamReader(stream); + var html = await reader.ReadToEndAsync(); + + // Replace base tag: -> + // Be robust with spaces or standard format + var modifiedHtml = html.Replace("", $"") + .Replace("", $""); + + return Results.Content(modifiedHtml, "text/html"); +}); + +// EN: Fallback to index.html for SPA routing (default culture) +// VI: Fallback đến index.html cho SPA routing (ngôn ngữ mặc định) +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Properties/launchSettings.json b/apps/web-client-tpos-net/src/WebClientTpos.Server/Properties/launchSettings.json new file mode 100644 index 00000000..dd5b1ebd --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5092", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7228;http://localhost:5092", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj new file mode 100644 index 00000000..75077d36 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.http b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.http new file mode 100644 index 00000000..c6f39281 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.http @@ -0,0 +1,6 @@ +@WebClientTpos.Server_HostAddress = http://localhost:5091 + +GET {{WebClientTpos.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.Development.json b/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.json b/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/yarp.json b/apps/web-client-tpos-net/src/WebClientTpos.Server/yarp.json new file mode 100644 index 00000000..cfcc2d7e --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/yarp.json @@ -0,0 +1,91 @@ +{ + "ReverseProxy": { + "Routes": { + "iam-route": { + "ClusterId": "iam-cluster", + "Match": { + "Path": "/api/iam/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/iam" + } + ] + }, + "auth-route": { + "ClusterId": "iam-cluster", + "Match": { + "Path": "/api/auth/{**catch-all}" + }, + "Transforms": [ + { + "PathPattern": "/api/v1/auth/{**catch-all}" + } + ] + }, + "merchant-route": { + "ClusterId": "merchant-cluster", + "Match": { + "Path": "/api/merchants/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/merchants" + } + ] + }, + "catalog-route": { + "ClusterId": "catalog-cluster", + "Match": { + "Path": "/api/catalog/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/catalog" + } + ] + }, + "order-route": { + "ClusterId": "order-cluster", + "Match": { + "Path": "/api/orders/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/orders" + } + ] + } + }, + "Clusters": { + "iam-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5101" + } + } + }, + "merchant-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5102" + } + } + }, + "catalog-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5103" + } + } + }, + "order-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5104" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/ApiResponse.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/ApiResponse.cs new file mode 100644 index 00000000..9f1e992c --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/ApiResponse.cs @@ -0,0 +1,58 @@ +namespace WebClientTpos.Shared; + +/// +/// EN: Standard API response wrapper for consistent response format. +/// VI: Wrapper response API chuẩn cho định dạng response nhất quán. +/// +/// The type of data in the response. +public class ApiResponse +{ + /// + /// EN: Indicates if the request was successful. + /// VI: Cho biết request có thành công không. + /// + public bool Success { get; set; } + + /// + /// EN: The response data. + /// VI: Dữ liệu response. + /// + public T? Data { get; set; } + + /// + /// EN: Error message if request failed. + /// VI: Thông báo lỗi nếu request thất bại. + /// + public string? Error { get; set; } + + /// + /// EN: Creates a successful response with data. + /// VI: Tạo response thành công với dữ liệu. + /// + public static ApiResponse Ok(T data) => new() { Success = true, Data = data }; + + /// + /// EN: Creates a failed response with error message. + /// VI: Tạo response thất bại với thông báo lỗi. + /// + public static ApiResponse Fail(string error) => new() { Success = false, Error = error }; +} + +/// +/// EN: Non-generic API response for operations without return data. +/// VI: API response không generic cho các operation không có dữ liệu trả về. +/// +public class ApiResponse : ApiResponse +{ + /// + /// EN: Creates a successful response. + /// VI: Tạo response thành công. + /// + public static new ApiResponse Ok() => new() { Success = true }; + + /// + /// EN: Creates a failed response with error message. + /// VI: Tạo response thất bại với thông báo lỗi. + /// + public static new ApiResponse Fail(string error) => new() { Success = false, Error = error }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ChangePasswordDto.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ChangePasswordDto.cs new file mode 100644 index 00000000..e2f66675 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ChangePasswordDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebClientTpos.Shared.DTOs; + +/// +/// EN: Change password DTO for authenticated users. +/// VI: DTO cho đổi mật khẩu của người dùng đã xác thực. +/// +public class ChangePasswordDto +{ + [Required(ErrorMessage = "Current password is required")] + public string CurrentPassword { get; set; } = string.Empty; + + [Required(ErrorMessage = "New password is required")] + [MinLength(8, ErrorMessage = "Password must be at least 8 characters")] + [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).+$", + ErrorMessage = "Password must contain uppercase, lowercase, digit, and special character")] + public string NewPassword { get; set; } = string.Empty; + + [Required(ErrorMessage = "Confirm password is required")] + [Compare(nameof(NewPassword), ErrorMessage = "Passwords do not match")] + public string ConfirmPassword { get; set; } = string.Empty; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ForgotPasswordDto.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ForgotPasswordDto.cs new file mode 100644 index 00000000..121cd779 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ForgotPasswordDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebClientTpos.Shared.DTOs; + +/// +/// EN: Forgot password request DTO. +/// VI: DTO cho yêu cầu quên mật khẩu. +/// +public class ForgotPasswordDto +{ + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email address")] + public string Email { get; set; } = string.Empty; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ProductDto.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ProductDto.cs new file mode 100644 index 00000000..ffd1dbbd --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ProductDto.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebClientTpos.Shared.DTOs; + +/// +/// EN: Product data transfer object with validation. +/// VI: DTO sản phẩm với validation. +/// +/// +/// EN: This DTO is shared between Client and Server for consistent validation. +/// VI: DTO này được chia sẻ giữa Client và Server để validation nhất quán. +/// +public class ProductDto +{ + /// + /// EN: Product name, required with length constraints. + /// VI: Tên sản phẩm, bắt buộc với ràng buộc độ dài. + /// + [Required(ErrorMessage = "Tên sản phẩm là bắt buộc / Name is required")] + [StringLength(100, MinimumLength = 3, ErrorMessage = "Tên phải từ 3-100 ký tự / Name must be 3-100 chars")] + public string Name { get; set; } = string.Empty; + + /// + /// EN: Product description. + /// VI: Mô tả sản phẩm. + /// + [StringLength(500, ErrorMessage = "Mô tả tối đa 500 ký tự / Max 500 characters")] + public string? Description { get; set; } + + /// + /// EN: Product price, must be positive. + /// VI: Giá sản phẩm, phải dương. + /// + [Required(ErrorMessage = "Giá là bắt buộc / Price is required")] + [Range(0.01, 1_000_000_000, ErrorMessage = "Giá phải từ 0.01 đến 1 tỷ / Price must be 0.01 to 1B")] + public decimal Price { get; set; } + + /// + /// EN: Stock quantity. + /// VI: Số lượng tồn kho. + /// + [Range(0, int.MaxValue, ErrorMessage = "Số lượng không âm / Quantity must be non-negative")] + public int Quantity { get; set; } +} + +/// +/// EN: Create product request DTO. +/// VI: DTO request tạo sản phẩm. +/// +public class CreateProductDto : ProductDto +{ + /// + /// EN: Product category ID. + /// VI: ID danh mục sản phẩm. + /// + [Required(ErrorMessage = "Danh mục là bắt buộc / Category is required")] + public Guid CategoryId { get; set; } +} + +/// +/// EN: Update product request DTO. +/// VI: DTO request cập nhật sản phẩm. +/// +public class UpdateProductDto : ProductDto +{ + /// + /// EN: Product ID. + /// VI: ID sản phẩm. + /// + [Required] + public Guid Id { get; set; } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ResetPasswordDto.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ResetPasswordDto.cs new file mode 100644 index 00000000..3f9078a8 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/ResetPasswordDto.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebClientTpos.Shared.DTOs; + +/// +/// EN: Reset password DTO. +/// VI: DTO cho đặt lại mật khẩu. +/// +public class ResetPasswordDto +{ + [Required(ErrorMessage = "Token is required")] + public string Token { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email address")] + public string Email { get; set; } = string.Empty; + + [Required(ErrorMessage = "Password is required")] + [MinLength(8, ErrorMessage = "Password must be at least 8 characters")] + [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).+$", + ErrorMessage = "Password must contain uppercase, lowercase, digit, and special character")] + public string NewPassword { get; set; } = string.Empty; + + [Required(ErrorMessage = "Confirm password is required")] + [Compare(nameof(NewPassword), ErrorMessage = "Passwords do not match")] + public string ConfirmPassword { get; set; } = string.Empty; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/UserDto.cs b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/UserDto.cs new file mode 100644 index 00000000..20ddcfb5 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/DTOs/UserDto.cs @@ -0,0 +1,93 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebClientTpos.Shared.DTOs; + +/// +/// EN: User registration DTO with validation. +/// VI: DTO đăng ký user với validation. +/// +public class RegisterDto +{ + /// + /// EN: User email address. + /// VI: Địa chỉ email user. + /// + [Required(ErrorMessage = "Email là bắt buộc / Email is required")] + [EmailAddress(ErrorMessage = "Email không hợp lệ / Invalid email format")] + public string Email { get; set; } = string.Empty; + + /// + /// EN: User password with strength requirements. + /// VI: Mật khẩu user với yêu cầu độ mạnh. + /// + [Required(ErrorMessage = "Mật khẩu là bắt buộc / Password is required")] + [StringLength(100, MinimumLength = 8, ErrorMessage = "Mật khẩu phải từ 8-100 ký tự / Password must be 8-100 chars")] + [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$", + ErrorMessage = "Mật khẩu phải có chữ hoa, chữ thường và số / Password must have upper, lower and digit")] + public string Password { get; set; } = string.Empty; + + /// + /// EN: Password confirmation must match. + /// VI: Xác nhận mật khẩu phải khớp. + /// + [Required(ErrorMessage = "Xác nhận mật khẩu là bắt buộc / Confirm password is required")] + [Compare(nameof(Password), ErrorMessage = "Mật khẩu không khớp / Passwords do not match")] + public string ConfirmPassword { get; set; } = string.Empty; + + /// + /// EN: User display name. + /// VI: Tên hiển thị của user. + /// + [Required(ErrorMessage = "Tên là bắt buộc / Name is required")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "Tên phải từ 2-50 ký tự / Name must be 2-50 chars")] + public string DisplayName { get; set; } = string.Empty; + + /// + /// EN: Accept terms of service. + /// VI: Chấp nhận điều khoản dịch vụ. + /// + [Range(typeof(bool), "true", "true", ErrorMessage = "Bạn phải chấp nhận Điều khoản dịch vụ / You must accept the Terms of Service")] + public bool AcceptTerms { get; set; } +} + +/// +/// EN: User login DTO. +/// VI: DTO đăng nhập user. +/// +public class LoginDto +{ + /// + /// EN: User email address. + /// VI: Địa chỉ email user. + /// + [Required(ErrorMessage = "Email là bắt buộc / Email is required")] + [EmailAddress(ErrorMessage = "Email không hợp lệ / Invalid email format")] + public string Email { get; set; } = string.Empty; + + /// + /// EN: User password. + /// VI: Mật khẩu user. + /// + [Required(ErrorMessage = "Mật khẩu là bắt buộc / Password is required")] + public string Password { get; set; } = string.Empty; + + /// + /// EN: Remember me option. + /// VI: Tùy chọn ghi nhớ đăng nhập. + /// + public bool RememberMe { get; set; } +} + +/// +/// EN: User profile DTO. +/// VI: DTO hồ sơ user. +/// +public class UserProfileDto +{ + public Guid Id { get; set; } + public string Email { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string? AvatarUrl { get; set; } + public DateTime CreatedAt { get; set; } + public bool EmailVerified { get; set; } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Shared/WebClientTpos.Shared.csproj b/apps/web-client-tpos-net/src/WebClientTpos.Shared/WebClientTpos.Shared.csproj new file mode 100644 index 00000000..b7601447 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Shared/WebClientTpos.Shared.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + +