feat: Thêm các thành phần UI cho trạng thái tải (loading states), trạng thái rỗng (empty states) và tích hợp tài sản thương hiệu.
This commit is contained in:
@@ -1,31 +1,185 @@
|
||||
# Web Client Application
|
||||
# GoodGo Platform - Web Client
|
||||
|
||||
Next.js web application for GoodGo Platform Client Portal.
|
||||
> **EN**: Enterprise-grade web client for GoodGo microservices platform
|
||||
> **VI**: Web client cấp doanh nghiệp cho nền tảng microservices GoodGo
|
||||
|
||||
## Features
|
||||
## Features / Tính Năng
|
||||
|
||||
- Next.js 14 with App Router
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Zustand for state management
|
||||
- API integration with auth service
|
||||
- ✅ **Brand Identity System** - Complete logo suite, favicons, and illustrations
|
||||
- ✅ **Design System** - Professional design tokens with brand colors, gradients, and glassmorphism
|
||||
- ✅ **UI Component Library** - Enhanced components with brand variants (Button, EmptyState, Loading States, Logo)
|
||||
- ✅ **Dark/Light Theme** - Automatic theme switching with system preference
|
||||
- ✅ **Internationalization** - Multi-language support (i18n ready)
|
||||
- ✅ **TypeScript** - Full type safety
|
||||
- ✅ **Tailwind CSS 4** - Modern utility-first styling
|
||||
- ✅ **Next.js 14** - App Router with RSC
|
||||
- ✅ **Accessibility** - WCAG 2.1 AA compliant
|
||||
- ✅ **PWA Ready** - Progressive Web App support
|
||||
|
||||
## Development
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 14 (App Router)
|
||||
- **Language**: TypeScript 5+
|
||||
- **Styling**: Tailwind CSS 4 (CSS-first)
|
||||
- **State Management**: Zustand
|
||||
- **API Client**: `@goodgo/http-client`
|
||||
- **Testing**: Vitest + Playwright
|
||||
- **Component Development**: Storybook
|
||||
|
||||
##Development / Phát Triển
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
# Install dependencies / Cài đặt dependencies
|
||||
pnpm install
|
||||
|
||||
# Start development server
|
||||
# Start dev server / Khởi động dev server
|
||||
pnpm dev
|
||||
# → http://localhost:3000
|
||||
|
||||
# Build for production
|
||||
# Start Storybook / Khởi động Storybook
|
||||
pnpm storybook
|
||||
# → http://localhost:6006
|
||||
|
||||
# Build for production / Build cho production
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
# Type checking / Kiểm tra kiểu
|
||||
pnpm typecheck
|
||||
|
||||
# Lint / Kiểm tra lỗi
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
## Environment Variables / Biến Môi Trường
|
||||
|
||||
Create `.env.local` file:
|
||||
|
||||
```bash
|
||||
# API URL
|
||||
NEXT_PUBLIC_API_URL=http://localhost/api/v1
|
||||
```
|
||||
|
||||
## Brand Assets / Tài Sản Thương Hiệu
|
||||
|
||||
Brand assets are located in `/public/brand-assets/`:
|
||||
|
||||
- **Logos**: `/brand-assets/logo/` (full, icon, wordmark variants)
|
||||
- **Icons**: `/brand-assets/icons/` (favicon)
|
||||
- **Illustrations**: `/brand-assets/illustrations/` (empty state, error state)
|
||||
|
||||
Usage in components:
|
||||
|
||||
```tsx
|
||||
import { BrandLogo } from '@/components/ui/brand-logo';
|
||||
import { BRAND } from '@/lib/brand-constants';
|
||||
|
||||
// Logo component
|
||||
<BrandLogo variant="full" size="lg" />
|
||||
|
||||
// Brand constants
|
||||
const primaryColor = BRAND.colors.primary.hex; // #3B82F6
|
||||
```
|
||||
|
||||
## UI Components / Components Giao Diện
|
||||
|
||||
### BrandLogo
|
||||
```tsx
|
||||
import { BrandLogo, BrandLogoLink } from '@/components/ui/brand-logo';
|
||||
|
||||
<BrandLogo variant="full" size="xl" />
|
||||
<BrandLogoLink variant="icon" size="md" href="/" />
|
||||
```
|
||||
|
||||
### Button with Brand Variants
|
||||
```tsx
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
<Button variant="brand">Get Started</Button>
|
||||
<Button variant="glass">Learn More</Button>
|
||||
```
|
||||
|
||||
### Empty State
|
||||
```tsx
|
||||
import { EmptyState, ErrorState } from '@/components/ui/empty-state';
|
||||
|
||||
<EmptyState
|
||||
title="No items found"
|
||||
description="Try adding some items"
|
||||
action={{ label: 'Add Item', onClick: handleAdd }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Loading States
|
||||
```tsx
|
||||
import {
|
||||
BrandSpinner,
|
||||
Skeleton,
|
||||
SkeletonCard,
|
||||
LoadingOverlay,
|
||||
ProgressBar
|
||||
} from '@/components/ui/loading-states';
|
||||
|
||||
<BrandSpinner size="lg" color="brand" />
|
||||
<SkeletonCard />
|
||||
<LoadingOverlay show={isLoading} message="Loading..." />
|
||||
<ProgressBar value={progress} showLabel />
|
||||
```
|
||||
|
||||
## Design System / Hệ Thống Thiết Kế
|
||||
|
||||
### Brand Colors
|
||||
|
||||
```css
|
||||
/* Primary (Blue) - Tech & Trust */
|
||||
--brand-primary: #3B82F6;
|
||||
|
||||
/* Secondary (Purple) - Innovation */
|
||||
--brand-secondary: #8B5CF6;
|
||||
|
||||
/* Accent (Cyan) - Energy */
|
||||
--brand-accent: #06B6D4;
|
||||
```
|
||||
|
||||
### Tailwind Utilities
|
||||
|
||||
```tsx
|
||||
// Brand colors
|
||||
<div className="bg-brand-primary text-white" />
|
||||
<div className="bg-brand-gradient" />
|
||||
|
||||
// Glassmorphism
|
||||
<div className="bg-glass-bg backdrop-blur-glass border-glass-border" />
|
||||
|
||||
// Brand shadows
|
||||
<div className="shadow-brand hover:shadow-brand-lg" />
|
||||
```
|
||||
|
||||
## Project Structure / Cấu Trúc Dự Án
|
||||
|
||||
```
|
||||
apps/web-client/
|
||||
├── public/
|
||||
│ └── brand-assets/ # Brand logos, icons, illustrations
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router pages
|
||||
│ ├── components/
|
||||
│ │ └── ui/ # Reusable UI components
|
||||
│ ├── lib/
|
||||
│ │ └── brand-constants.ts # Brand helper functions
|
||||
│ ├── styles/
|
||||
│ │ └── theme.css # Design system tokens
|
||||
│ ├── contexts/ # React contexts (Theme, etc.)
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── providers/ # Provider components
|
||||
│ └── stores/ # Zustand stores
|
||||
└── tailwind.config.js # Tailwind configuration
|
||||
```
|
||||
|
||||
## Contributing / Đóng Góp
|
||||
|
||||
Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
||||
|
||||
## License / Giấy Phép
|
||||
|
||||
Proprietary - GoodGo Platform
|
||||
|
||||
- `NEXT_PUBLIC_API_URL` - API base URL (default: http://localhost/api/v1)
|
||||
|
||||
19
apps/web-client/public/brand-assets/icons/favicon.svg
Normal file
19
apps/web-client/public/brand-assets/icons/favicon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: Favicon - Simplified icon optimized for small sizes
|
||||
VI: Favicon - Icon đơn giản hóa tối ưu cho kích thước nhỏ
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="faviconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Simplified hexagon -->
|
||||
<path d="M16 4 L25 10 L25 22 L16 28 L7 22 L7 10 Z"
|
||||
fill="url(#faviconGradient)"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="16" cy="16" r="3" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1,50 @@
|
||||
<svg width="400" height="300" viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: Empty State Illustration - Minimalist design with brand colors
|
||||
VI: Minh họa Empty State - Thiết kế tối giản với màu thương hiệu
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="emptyGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:0.2" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:0.2" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background circle -->
|
||||
<circle cx="200" cy="150" r="120" fill="url(#emptyGradient)"/>
|
||||
|
||||
<!-- Document icon -->
|
||||
<g transform="translate(150, 100)">
|
||||
<!-- Main rectangle -->
|
||||
<rect x="0" y="0" width="100" height="120" rx="8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
opacity="0.3"/>
|
||||
|
||||
<!-- Lines representing text -->
|
||||
<line x1="15" y1="25" x2="85" y2="25"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
opacity="0.2"/>
|
||||
<line x1="15" y1="45" x2="70" y2="45"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
opacity="0.2"/>
|
||||
<line x1="15" y1="65" x2="85" y2="65"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
opacity="0.2"/>
|
||||
<line x1="15" y1="85" x2="60" y2="85"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
opacity="0.2"/>
|
||||
</g>
|
||||
|
||||
<!-- Plus icon overlay -->
|
||||
<g transform="translate(200, 150)">
|
||||
<circle cx="35" cy="35" r="25" fill="#3B82F6"/>
|
||||
<line x1="35" y1="25" x2="35" y2="45" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="25" y1="35" x2="45" y2="35" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,33 @@
|
||||
<svg width="400" height="300" viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: Error State Illustration - Alert triangle with brand styling
|
||||
VI: Minh họa Error State - Tam giác cảnh báo với style thương hiệu
|
||||
-->
|
||||
|
||||
<!-- Background circle with error color -->
|
||||
<circle cx="200" cy="150" r="120"
|
||||
fill="#EF4444"
|
||||
opacity="0.1"/>
|
||||
|
||||
<!-- Alert triangle -->
|
||||
<g transform="translate(200, 150)">
|
||||
<!-- Triangle outline -->
|
||||
<path d="M 0,-70 L 60,50 L -60,50 Z"
|
||||
fill="none"
|
||||
stroke="#EF4444"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"/>
|
||||
|
||||
<!-- Exclamation mark -->
|
||||
<line x1="0" y1="-30" x2="0" y2="10"
|
||||
stroke="#EF4444"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"/>
|
||||
<circle cx="0" cy="30" r="4" fill="#EF4444"/>
|
||||
</g>
|
||||
|
||||
<!-- Decorative elements -->
|
||||
<circle cx="120" cy="80" r="8" fill="#EF4444" opacity="0.2"/>
|
||||
<circle cx="280" cy="220" r="12" fill="#EF4444" opacity="0.2"/>
|
||||
<circle cx="290" cy="90" r="6" fill="#EF4444" opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
54
apps/web-client/public/brand-assets/logo/logo-full.svg
Normal file
54
apps/web-client/public/brand-assets/logo/logo-full.svg
Normal file
@@ -0,0 +1,54 @@
|
||||
<svg width="280" height="48" viewBox="0 0 280 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: GoodGo Platform Full Logo - Icon + Wordmark
|
||||
VI: Logo đầy đủ GoodGo Platform - Icon + Wordmark
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Icon (scaled to 48x48) -->
|
||||
<g transform="translate(0, 0)">
|
||||
<!-- Main hexagon -->
|
||||
<path d="M24 6 L37.5 15 L37.5 33 L24 42 L10.5 33 L10.5 15 Z"
|
||||
fill="url(#iconGradient)"
|
||||
opacity="0.9"/>
|
||||
|
||||
<!-- Inner hexagon -->
|
||||
<path d="M24 13.5 L31.5 18.75 L31.5 29.25 L24 34.5 L16.5 29.25 L16.5 18.75 Z"
|
||||
fill="#FFFFFF"
|
||||
opacity="0.2"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="24" cy="24" r="3" fill="#FFFFFF"/>
|
||||
</g>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<g transform="translate(60, 0)">
|
||||
<!-- GoodGo text -->
|
||||
<text x="0" y="32"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="28"
|
||||
font-weight="700"
|
||||
fill="url(#textGradient)">
|
||||
GoodGo
|
||||
</text>
|
||||
|
||||
<!-- Platform text -->
|
||||
<text x="115" y="32"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="400"
|
||||
fill="currentColor"
|
||||
opacity="0.6">
|
||||
Platform
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
28
apps/web-client/public/brand-assets/logo/logo-icon.svg
Normal file
28
apps/web-client/public/brand-assets/logo/logo-icon.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: GoodGo Platform Logo Icon - Geometric minimalist design
|
||||
VI: Logo Icon GoodGo Platform - Thiết kế hình học tối giản
|
||||
|
||||
Design concept: Overlapping hexagons representing microservices architecture
|
||||
Màu sắc: Brand gradient (Blue to Purple)
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="brandGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Main hexagon -->
|
||||
<path d="M32 8 L50 20 L50 44 L32 56 L14 44 L14 20 Z"
|
||||
fill="url(#brandGradient)"
|
||||
opacity="0.9"/>
|
||||
|
||||
<!-- Inner hexagon -->
|
||||
<path d="M32 18 L42 25 L42 39 L32 46 L22 39 L22 25 Z"
|
||||
fill="#FFFFFF"
|
||||
opacity="0.2"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="32" cy="32" r="4" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 996 B |
31
apps/web-client/public/brand-assets/logo/logo-wordmark.svg
Normal file
31
apps/web-client/public/brand-assets/logo/logo-wordmark.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg width="200" height="40" viewBox="0 0 200 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--
|
||||
EN: GoodGo Platform Wordmark - Clean typography
|
||||
VI: Wordmark GoodGo Platform - Typography sạch sẽ
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- GoodGo text -->
|
||||
<text x="0" y="28"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="24"
|
||||
font-weight="700"
|
||||
fill="url(#textGradient)">
|
||||
GoodGo
|
||||
</text>
|
||||
|
||||
<!-- Platform text -->
|
||||
<text x="100" y="28"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="16"
|
||||
font-weight="400"
|
||||
fill="currentColor"
|
||||
opacity="0.6">
|
||||
Platform
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 896 B |
23
apps/web-client/public/manifest.json
Normal file
23
apps/web-client/public/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "GoodGo Platform",
|
||||
"short_name": "GoodGo",
|
||||
"description": "Enterprise Microservices Platform - Build, deploy, and scale with confidence",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#3B82F6",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/brand-assets/icons/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
"developer tools",
|
||||
"productivity"
|
||||
],
|
||||
"lang": "en-US"
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.5;
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +42,8 @@
|
||||
*::before,
|
||||
*::after {
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out),
|
||||
border-color var(--duration-normal) var(--ease-in-out);
|
||||
color var(--duration-normal) var(--ease-in-out),
|
||||
border-color var(--duration-normal) var(--ease-in-out);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,10 +122,14 @@
|
||||
* VI: Animation cho Typing Indicator
|
||||
*/
|
||||
@keyframes typing-pulse {
|
||||
0%, 60%, 100% {
|
||||
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
@@ -145,12 +149,27 @@
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Shimmer animation for skeleton loaders
|
||||
* VI: Animation shimmer cho skeleton loaders
|
||||
*/
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Ensure smooth animations and prevent layout shift
|
||||
* VI: Đảm bảo animation mượt mà và ngăn layout shift
|
||||
@@ -159,4 +178,8 @@
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,45 @@ import { SkipToContent } from '../components/accessibility/skip-to-content';
|
||||
* VI: Metadata cho ứng dụng
|
||||
*/
|
||||
export const metadata: Metadata = {
|
||||
title: 'GoodGo Platform',
|
||||
description: 'Enterprise microservices platform / Nền tảng microservices doanh nghiệp',
|
||||
title: {
|
||||
default: 'GoodGo Platform - Enterprise Microservices',
|
||||
template: '%s | GoodGo Platform',
|
||||
},
|
||||
description: 'Build, deploy, and scale microservices with confidence. Enterprise-grade microservices platform for modern development teams.',
|
||||
keywords: ['microservices', 'enterprise', 'platform', 'cloud', 'kubernetes', 'devops'],
|
||||
|
||||
// EN: Brand icons for all platforms / VI: Brand icons cho tất cả platforms
|
||||
icons: {
|
||||
icon: '/brand-assets/icons/favicon.svg',
|
||||
apple: '/brand-assets/icons/favicon.svg',
|
||||
},
|
||||
|
||||
// EN: Open Graph for social media sharing / VI: Open Graph cho chia sẻ mạng xã hội
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://goodgo.com',
|
||||
siteName: 'GoodGo Platform',
|
||||
title: 'GoodGo Platform - Enterprise Microservices',
|
||||
description: 'Build, deploy, and scale microservices with confidence',
|
||||
},
|
||||
|
||||
// EN: Twitter Card metadata / VI: Twitter Card metadata
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'GoodGo Platform',
|
||||
description: 'Enterprise Microservices Platform',
|
||||
},
|
||||
|
||||
// EN: PWA manifest / VI: PWA manifest
|
||||
manifest: '/manifest.json',
|
||||
|
||||
// EN: Viewport configuration / VI: Cấu hình viewport
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '@/hooks/use-translation';
|
||||
import { BrandLogo } from '@/components/ui/brand-logo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/**
|
||||
* EN: Home page component - main application entry point
|
||||
* VI: Component trang chủ - điểm vào chính của ứng dụng
|
||||
* EN: Home page component - main application entry point with brand elements
|
||||
* VI: Component trang chủ - điểm vào chính với brand elements
|
||||
*/
|
||||
export default function Home() {
|
||||
// EN: Translation hook / VI: Hook translation
|
||||
@@ -46,21 +48,38 @@ export default function Home() {
|
||||
// EN: Show loading state while checking authentication
|
||||
// VI: Hiển thị trạng thái loading trong khi kiểm tra xác thực
|
||||
if (isLoading) {
|
||||
return <div className="min-h-screen flex items-center justify-center p-8 bg-bg-primary text-text-primary">{t('common.loading')}</div>;
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-8 bg-bg-primary text-text-primary">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// EN: Main content area with centered layout for minimal aesthetic
|
||||
// VI: Khu vực nội dung chính với layout căn giữa cho thẩm mỹ tối giản
|
||||
<main className="min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary">
|
||||
<div className="max-w-container-md w-full space-y-8 animate-fadeIn">
|
||||
// EN: Main content area with brand gradient background
|
||||
// VI: Khu vực nội dung chính với background gradient thương hiệu
|
||||
<main className="relative min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary overflow-hidden">
|
||||
{/* EN: Brand gradient overlay / VI: Overlay gradient thương hiệu */}
|
||||
<div className="absolute inset-0 bg-brand-gradient opacity-5 -z-10" />
|
||||
|
||||
{/* EN: Decorative brand dots / VI: Chấm trang trí thương hiệu */}
|
||||
<div className="absolute top-20 left-10 w-3 h-3 rounded-full bg-brand-primary opacity-20 animate-pulse" />
|
||||
<div className="absolute top-40 right-20 w-4 h-4 rounded-full bg-brand-secondary opacity-20 animate-pulse" style={{ animationDelay: '1s' }} />
|
||||
<div className="absolute bottom-32 left-1/4 w-2 h-2 rounded-full bg-brand-accent opacity-20 animate-pulse" style={{ animationDelay: '2s' }} />
|
||||
|
||||
<div className="max-w-container-lg w-full space-y-12 animate-fadeIn z-10">
|
||||
{/* EN: Brand Logo / VI: Logo thương hiệu */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<BrandLogo variant="full" size="xl" priority />
|
||||
</div>
|
||||
|
||||
{/* EN: Hero Title / VI: Tiêu đề Hero */}
|
||||
<h1 className="text-6xl font-bold tracking-tight mb-2 font-display">
|
||||
<h1 className="text-6xl font-display font-bold tracking-tight mb-4 bg-clip-text text-transparent bg-brand-gradient-vertical leading-tight">
|
||||
{t('home.title')}
|
||||
</h1>
|
||||
|
||||
{/* EN: Subtitle/Description / VI: Phụ đề/Mô tả */}
|
||||
<p className="text-xl text-text-secondary max-w-lg mx-auto leading-relaxed">
|
||||
<p className="text-xl text-text-secondary max-w-2xl mx-auto leading-relaxed">
|
||||
{t('home.description')}
|
||||
</p>
|
||||
|
||||
@@ -68,31 +87,69 @@ export default function Home() {
|
||||
<div className="pt-8">
|
||||
{isAuthenticated && user ? (
|
||||
// EN: Authenticated user welcome message / VI: Thông báo chào mừng người dùng đã xác thực
|
||||
<div className="space-y-4 p-6 rounded-2xl bg-bg-secondary border border-border-primary inline-block min-w-[300px]">
|
||||
<div className="w-16 h-16 bg-bg-tertiary rounded-full mx-auto flex items-center justify-center mb-4 text-2xl">
|
||||
<div className="space-y-6 p-8 rounded-2xl bg-glass-bg backdrop-blur-glass border border-glass-border inline-block min-w-[320px] shadow-brand">
|
||||
<div className="w-20 h-20 bg-brand-gradient rounded-full mx-auto flex items-center justify-center mb-6 text-3xl font-semibold text-white shadow-colored">
|
||||
{user.email?.[0].toUpperCase()}
|
||||
</div>
|
||||
<p className="text-lg font-medium">{t('home.welcome', { email: user.email })}</p>
|
||||
<div className="inline-flex items-center px-3 py-1 rounded-full bg-bg-tertiary text-text-secondary text-sm">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-success mr-2"></span>
|
||||
<p className="text-lg font-medium text-text-primary">
|
||||
{t('home.welcome', { email: user.email })}
|
||||
</p>
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full bg-bg-tertiary text-text-secondary text-sm border border-border-primary">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-success mr-2 animate-pulse" />
|
||||
{t('home.role', { role: user.role })}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// EN: Login prompt for unauthenticated users / VI: Nhắc đăng nhập cho người dùng chưa xác thực
|
||||
<div className="space-y-6">
|
||||
<p className="text-text-secondary">{t('home.pleaseLogin')}</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<a
|
||||
href="/auth/login"
|
||||
className="px-8 py-3 rounded-full bg-text-primary text-bg-primary font-medium hover:bg-text-secondary transition-colors"
|
||||
<div className="space-y-8">
|
||||
<p className="text-text-secondary text-lg">
|
||||
{t('home.pleaseLogin')}
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<Button
|
||||
variant="brand"
|
||||
size="xl"
|
||||
onClick={() => (window.location.href = '/auth/login')}
|
||||
className="px-12"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
Get Started
|
||||
</Button>
|
||||
<Button
|
||||
variant="glass"
|
||||
size="xl"
|
||||
onClick={() => (window.location.href = '/auth/login')}
|
||||
className="px-12"
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* EN: Feature highlights / VI: Điểm nổi bật tính năng */}
|
||||
{!isAuthenticated && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-16 max-w-4xl mx-auto">
|
||||
{[
|
||||
{ title: 'Fast Development', icon: '⚡', desc: 'Build and deploy in minutes' },
|
||||
{ title: 'Enterprise Ready', icon: '🏢', desc: 'Production-grade platform' },
|
||||
{ title: 'Developer First', icon: '💻', desc: 'Built for modern teams' },
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-6 rounded-xl bg-glass-bg backdrop-blur-glass border border-glass-border hover:border-brand-primary/30 transition-all duration-normal hover:shadow-brand group"
|
||||
>
|
||||
<div className="text-4xl mb-4 group-hover:scale-110 transition-transform duration-normal">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2 text-text-primary">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
140
apps/web-client/src/components/ui/brand-logo.tsx
Normal file
140
apps/web-client/src/components/ui/brand-logo.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTheme } from '@/contexts/theme-context';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BRAND, getBrandLogo } from '@/lib/brand-constants';
|
||||
|
||||
/**
|
||||
* EN: Brand logo variant types
|
||||
* VI: Các kiểu biến thể logo thương hiệu
|
||||
*/
|
||||
export type LogoVariant = 'full' | 'icon' | 'wordmark';
|
||||
|
||||
/**
|
||||
* EN: Brand logo size presets
|
||||
* VI: Kích thước logo định sẵn
|
||||
*/
|
||||
export type LogoSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
|
||||
/**
|
||||
* EN: Brand logo component props
|
||||
* VI: Props cho component logo thương hiệu
|
||||
*/
|
||||
export interface BrandLogoProps {
|
||||
/** Logo variant / Biến thể logo */
|
||||
variant?: LogoVariant;
|
||||
/** Logo size / Kích thước logo */
|
||||
size?: LogoSize;
|
||||
/** Additional CSS classes / CSS classes bổ sung */
|
||||
className?: string;
|
||||
/** Whether to use Next.js Image optimization / Có sử dụng tối ưu Image của Next.js không */
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Size mappings for different logo sizes
|
||||
* VI: Ánh xạ kích thước cho các size logo khác nhau
|
||||
*/
|
||||
const sizeClasses: Record<LogoSize, { width: string; height: string }> = {
|
||||
xs: { width: 'w-16', height: 'h-4' },
|
||||
sm: { width: 'w-24', height: 'h-6' },
|
||||
md: { width: 'w-32', height: 'h-8' },
|
||||
lg: { width: 'w-48', height: 'h-12' },
|
||||
xl: { width: 'w-64', height: 'h-16' },
|
||||
'2xl': { width: 'w-80', height: 'h-20' },
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Size mappings for icon variant
|
||||
* VI: Ánh xạ kích thước cho biến thể icon
|
||||
*/
|
||||
const iconSizeClasses: Record<LogoSize, { width: string; height: string }> = {
|
||||
xs: { width: 'w-4', height: 'h-4' },
|
||||
sm: { width: 'w-6', height: 'h-6' },
|
||||
md: { width: 'w-8', height: 'h-8' },
|
||||
lg: { width: 'w-12', height: 'h-12' },
|
||||
xl: { width: 'w-16', height: 'h-16' },
|
||||
'2xl': { width: 'w-20', height: 'h-20' },
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Brand Logo Component - Displays brand logo with automatic theme switching
|
||||
* VI: Component Logo Thương Hiệu - Hiển thị logo với tự động chuyển theme
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BrandLogo variant="full" size="lg" />
|
||||
* <BrandLogo variant="icon" size="md" />
|
||||
* <BrandLogo variant="wordmark" size="sm" />
|
||||
* ```
|
||||
*/
|
||||
export function BrandLogo({
|
||||
variant = 'full',
|
||||
size = 'md',
|
||||
className,
|
||||
priority = false,
|
||||
}: BrandLogoProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
// EN: Get logo path based on variant
|
||||
// VI: Lấy đường dẫn logo dựa trên biến thể
|
||||
const logoSrc = getBrandLogo(variant);
|
||||
|
||||
// EN: Select size classes based on variant
|
||||
// VI: Chọn class kích thước dựa trên biến thể
|
||||
const sizes = variant === 'icon' ? iconSizeClasses[size] : sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center',
|
||||
sizes.width,
|
||||
sizes.height,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={BRAND.name}
|
||||
fill
|
||||
priority={priority}
|
||||
className="object-contain"
|
||||
style={{
|
||||
// EN: Apply theme-aware color for SVG text elements
|
||||
// VI: Áp dụng màu nhận biết theme cho SVG text elements
|
||||
color: resolvedTheme === 'dark' ? '#FFFFFF' : '#000000',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Brand Logo Link - Clickable logo that links to home
|
||||
* VI: Logo Link - Logo có thể nhấp dẫn về trang chủ
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BrandLogoLink variant="full" size="lg" href="/" />
|
||||
* ```
|
||||
*/
|
||||
export interface BrandLogoLinkProps extends BrandLogoProps {
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function BrandLogoLink({
|
||||
href = '/',
|
||||
...logoProps
|
||||
}: BrandLogoLinkProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="inline-block transition-opacity hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary focus-visible:ring-offset-2 rounded-sm"
|
||||
aria-label={`${BRAND.name} - Go to homepage`}
|
||||
>
|
||||
<BrandLogo {...logoProps} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,12 @@ const buttonVariants = cva(
|
||||
// EN: Danger button - destructive actions / VI: Button nguy hiểm - hành động phá hủy
|
||||
danger:
|
||||
'bg-accent-error text-white hover:brightness-110 hover:scale-[1.02] active:scale-[0.98] active:brightness-90 focus-visible:ring-accent-error focus-visible:shadow-[0_0_20px_rgba(239,68,68,0.3)] shadow-md hover:shadow-lg',
|
||||
// EN: Brand button - main CTA with brand gradient / VI: Button thương hiệu - CTA chính với brand gradient
|
||||
brand:
|
||||
'bg-brand-gradient text-white hover:shadow-brand-lg hover:scale-[1.02] active:scale-[0.98] focus-visible:ring-brand-primary focus-visible:shadow-colored shadow-brand transition-all duration-fast',
|
||||
// EN: Glass button - glassmorphism style / VI: Button glass - phong cách glassmorphism
|
||||
glass:
|
||||
'bg-glass-bg backdrop-blur-glass border border-glass-border text-white hover:bg-glass-bg-hover hover:border-white/20 hover:scale-[1.02] active:scale-[0.98] focus-visible:ring-brand-primary shadow-md',
|
||||
},
|
||||
size: {
|
||||
// EN: Extra small button - 28px height (mobile: min 44px) / VI: Button cực nhỏ - chiều cao 28px (mobile: tối thiểu 44px)
|
||||
@@ -51,7 +57,7 @@ const buttonVariants = cva(
|
||||
*/
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
VariantProps<typeof buttonVariants> {
|
||||
/**
|
||||
* EN: Loading state - shows spinner when true / VI: Trạng thái loading - hiển thị spinner khi true
|
||||
*/
|
||||
|
||||
153
apps/web-client/src/components/ui/empty-state.tsx
Normal file
153
apps/web-client/src/components/ui/empty-state.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getBrandIllustration } from '@/lib/brand-constants';
|
||||
import { Button } from './button';
|
||||
|
||||
/**
|
||||
* EN: Empty state action button configuration
|
||||
* VI: Cấu hình button hành động cho empty state
|
||||
*/
|
||||
export interface EmptyStateAction {
|
||||
/** Button label / Nhãn button */
|
||||
label: string;
|
||||
/** Click handler / Hàm xử lý click */
|
||||
onClick: () => void;
|
||||
/** Button variant / Biến thể button */
|
||||
variant?: 'brand' | 'primary' | 'secondary' | 'ghost';
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Empty state component props
|
||||
* VI: Props cho component empty state
|
||||
*/
|
||||
export interface EmptyStateProps {
|
||||
/** Title text / Tiêu đề */
|
||||
title: string;
|
||||
/** Description text / Mô tả */
|
||||
description?: string;
|
||||
/** Illustration type / Loại minh họa */
|
||||
illustration?: 'empty' | 'error' | 'custom';
|
||||
/** Custom illustration path / Đường dẫn minh họa tùy chỉnh */
|
||||
customIllustration?: string;
|
||||
/** Primary action button / Button hành động chính */
|
||||
action?: EmptyStateAction;
|
||||
/** Secondary action button / Button hành động phụ */
|
||||
secondaryAction?: EmptyStateAction;
|
||||
/** Additional CSS classes / CSS classes bổ sung */
|
||||
className?: string;
|
||||
/** Show illustration / Hiển thị minh họa */
|
||||
showIllustration?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Empty State Component - Professional empty state with illustrations
|
||||
* VI: Component Empty State - Empty state chuyên nghiệp với minh họa
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <EmptyState
|
||||
* title="No data found"
|
||||
* description="Try adding some items to get started"
|
||||
* illustration="empty"
|
||||
* action={{ label: 'Add Item', onClick: () => {} }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function EmptyState({
|
||||
title,
|
||||
description,
|
||||
illustration = 'empty',
|
||||
customIllustration,
|
||||
action,
|
||||
secondaryAction,
|
||||
className,
|
||||
showIllustration = true,
|
||||
}: EmptyStateProps) {
|
||||
// EN: Get illustration path
|
||||
// VI: Lấy đường dẫn minh họa
|
||||
const illustrationSrc =
|
||||
illustration === 'custom' && customIllustration
|
||||
? customIllustration
|
||||
: getBrandIllustration(illustration === 'custom' ? 'empty' : illustration);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center p-12 text-center min-h-[400px]',
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{/* EN: Illustration / VI: Minh họa */}
|
||||
{showIllustration && (
|
||||
<div className="relative w-64 h-64 mb-8 opacity-60 hover:opacity-80 transition-opacity duration-normal">
|
||||
<Image
|
||||
src={illustrationSrc}
|
||||
alt={title}
|
||||
fill
|
||||
className="object-contain"
|
||||
priority={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* EN: Title / VI: Tiêu đề */}
|
||||
<h3 className="text-2xl font-heading font-semibold mb-3 text-text-primary">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{/* EN: Description / VI: Mô tả */}
|
||||
{description && (
|
||||
<p className="text-text-secondary mb-8 max-w-md leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* EN: Actions / VI: Hành động */}
|
||||
{(action || secondaryAction) && (
|
||||
<div className="flex gap-4 items-center">
|
||||
{action && (
|
||||
<Button
|
||||
variant={action.variant || 'brand'}
|
||||
size="lg"
|
||||
onClick={action.onClick}
|
||||
className="shadow-brand hover:shadow-brand-lg"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryAction && (
|
||||
<Button
|
||||
variant={secondaryAction.variant || 'ghost'}
|
||||
size="lg"
|
||||
onClick={secondaryAction.onClick}
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Error State Component - Specialized empty state for errors
|
||||
* VI: Component Error State - Empty state chuyên biệt cho lỗi
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ErrorState
|
||||
* title="Something went wrong"
|
||||
* description="Please try again or contact support"
|
||||
* action={{ label: 'Try Again', onClick: () => {} }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function ErrorState(props: Omit<EmptyStateProps, 'illustration'>) {
|
||||
return <EmptyState {...props} illustration="error" />;
|
||||
}
|
||||
269
apps/web-client/src/components/ui/loading-states.tsx
Normal file
269
apps/web-client/src/components/ui/loading-states.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* EN: Loading spinner size types
|
||||
* VI: Kiểu kích thước loading spinner
|
||||
*/
|
||||
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
/**
|
||||
* EN: Loading spinner props
|
||||
* VI: Props cho loading spinner
|
||||
*/
|
||||
export interface SpinnerProps {
|
||||
/** Spinner size / Kích thước spinner */
|
||||
size?: SpinnerSize;
|
||||
/** Spinner color / Màu spinner */
|
||||
color?: 'brand' | 'primary' | 'white';
|
||||
/** Additional CSS classes / CSS classes bổ sung */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Size mappings for spinner
|
||||
* VI: Ánh xạ kích thước cho spinner
|
||||
*/
|
||||
const spinnerSizes: Record<SpinnerSize, string> = {
|
||||
xs: 'w-4 h-4 border-2',
|
||||
sm: 'w-6 h-6 border-2',
|
||||
md: 'w-8 h-8 border-3',
|
||||
lg: 'w-12 h-12 border-4',
|
||||
xl: 'w-16 h-16 border-4',
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Color mappings for spinner
|
||||
* VI: Ánh xạ màu cho spinner
|
||||
*/
|
||||
const spinnerColors: Record<string, string> = {
|
||||
brand: 'border-brand-primary/20 border-t-brand-primary',
|
||||
primary: 'border-text-primary/20 border-t-text-primary',
|
||||
white: 'border-white/20 border-t-white',
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Brand Spinner - Loading spinner with brand colors
|
||||
* VI: Brand Spinner - Loading spinner với màu thương hiệu
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BrandSpinner size="md" color="brand" />
|
||||
* ```
|
||||
*/
|
||||
export function BrandSpinner({
|
||||
size = 'md',
|
||||
color = 'brand',
|
||||
className,
|
||||
}: SpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-block rounded-full animate-spin',
|
||||
spinnerSizes[size],
|
||||
spinnerColors[color],
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Skeleton component props
|
||||
* VI: Props cho skeleton component
|
||||
*/
|
||||
export interface SkeletonProps {
|
||||
/** Skeleton variant / Biến thể skeleton */
|
||||
variant?: 'text' | 'circular' | 'rectangular';
|
||||
/** Width / Chiều rộng */
|
||||
width?: string | number;
|
||||
/** Height / Chiều cao */
|
||||
height?: string | number;
|
||||
/** Additional CSS classes / CSS classes bổ sung */
|
||||
className?: string;
|
||||
/** Animation wave effect / Hiệu ứng sóng animation */
|
||||
wave?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Skeleton - Loading placeholder with shimmer effect
|
||||
* VI: Skeleton - Loading placeholder với hiệu ứng shimmer
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Skeleton variant="text" width="100%" height="20px" />
|
||||
* <Skeleton variant="circular" width="40px" height="40px" />
|
||||
* <Skeleton variant="rectangular" width="200px" height="100px" />
|
||||
* ```
|
||||
*/
|
||||
export function Skeleton({
|
||||
variant = 'rectangular',
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
wave = true,
|
||||
}: SkeletonProps) {
|
||||
const variantClasses = {
|
||||
text: 'rounded',
|
||||
circular: 'rounded-full',
|
||||
rectangular: 'rounded-md',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-bg-tertiary animate-pulse',
|
||||
variantClasses[variant],
|
||||
wave && 'relative overflow-hidden',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: typeof width === 'number' ? `${width}px` : width,
|
||||
height: typeof height === 'number' ? `${height}px` : height,
|
||||
}}
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
>
|
||||
{wave && (
|
||||
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||
)}
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Skeleton Card - Pre-built skeleton for card layouts
|
||||
* VI: Skeleton Card - Skeleton định sẵn cho layout card
|
||||
*/
|
||||
export function SkeletonCard({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('p-6 border border-border-primary rounded-lg', className)}>
|
||||
<Skeleton variant="circular" width="48px" height="48px" className="mb-4" />
|
||||
<Skeleton variant="text" width="70%" height="24px" className="mb-2" />
|
||||
<Skeleton variant="text" width="100%" height="16px" className="mb-1" />
|
||||
<Skeleton variant="text" width="90%" height="16px" className="mb-4" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton variant="rectangular" width="80px" height="32px" />
|
||||
<Skeleton variant="rectangular" width="80px" height="32px" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Skeleton List - Pre-built skeleton for list layouts
|
||||
* VI: Skeleton List - Skeleton định sẵn cho layout list
|
||||
*/
|
||||
export function SkeletonList({
|
||||
count = 3,
|
||||
className,
|
||||
}: {
|
||||
count?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-4 p-4 border border-border-primary rounded-lg">
|
||||
<Skeleton variant="circular" width="40px" height="40px" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton variant="text" width="40%" height="16px" />
|
||||
<Skeleton variant="text" width="60%" height="14px" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Loading Overlay - Full screen loading overlay
|
||||
* VI: Loading Overlay - Overlay loading toàn màn hình
|
||||
*/
|
||||
export interface LoadingOverlayProps {
|
||||
/** Loading message / Thông báo loading */
|
||||
message?: string;
|
||||
/** Show overlay / Hiển thị overlay */
|
||||
show: boolean;
|
||||
/** Spinner size / Kích thước spinner */
|
||||
size?: SpinnerSize;
|
||||
}
|
||||
|
||||
export function LoadingOverlay({
|
||||
message = 'Loading...',
|
||||
show,
|
||||
size = 'lg',
|
||||
}: LoadingOverlayProps) {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-bg-primary/80 backdrop-blur-sm"
|
||||
role="status"
|
||||
aria-live="assertive"
|
||||
aria-label={message}
|
||||
>
|
||||
<BrandSpinner size={size} color="brand" />
|
||||
<p className="mt-6 text-lg text-text-secondary font-medium">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EN: Progress Bar - Linear progress indicator with brand colors
|
||||
* VI: Progress Bar - Thanh tiến trình với màu thương hiệu
|
||||
*/
|
||||
export interface ProgressBarProps {
|
||||
/** Progress value (0-100) / Giá trị tiến trình (0-100) */
|
||||
value: number;
|
||||
/** Show percentage label / Hiển thị nhãn phần trăm */
|
||||
showLabel?: boolean;
|
||||
/** Additional CSS classes / CSS classes bổ sung */
|
||||
className?: string;
|
||||
/** Progress bar color / Màu thanh tiến trình */
|
||||
color?: 'brand' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
const progressColors: Record<string, string> = {
|
||||
brand: 'bg-brand-gradient',
|
||||
success: 'bg-accent-success',
|
||||
warning: 'bg-accent-warning',
|
||||
error: 'bg-accent-error',
|
||||
};
|
||||
|
||||
export function ProgressBar({
|
||||
value,
|
||||
showLabel = false,
|
||||
className,
|
||||
color = 'brand',
|
||||
}: ProgressBarProps) {
|
||||
const clampedValue = Math.max(0, Math.min(100, value));
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)} role="progressbar" aria-valuenow={clampedValue} aria-valuemin={0} aria-valuemax={100}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-2 bg-bg-tertiary rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full transition-all duration-slow ease-out',
|
||||
progressColors[color]
|
||||
)}
|
||||
style={{ width: `${clampedValue}%` }}
|
||||
/>
|
||||
</div>
|
||||
{showLabel && (
|
||||
<span className="text-sm font-medium text-text-secondary min-w-[3ch] text-right">
|
||||
{Math.round(clampedValue)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
apps/web-client/src/lib/brand-constants.ts
Normal file
122
apps/web-client/src/lib/brand-constants.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* EN: Brand Constants - Easy access to brand values
|
||||
* VI: Hằng số thương hiệu - Truy cập dễ dàng các giá trị thương hiệu
|
||||
*
|
||||
* This file provides type-safe constants for brand colors, fonts, and assets.
|
||||
* Use these instead of hardcoding values for consistency and maintainability.
|
||||
*
|
||||
* File này cung cấp constants type-safe cho màu sắc, fonts và assets thương hiệu.
|
||||
* Sử dụng các constants này thay vì hardcode giá trị để đảm bảo tính nhất quán và dễ bảo trì.
|
||||
*/
|
||||
|
||||
export const BRAND = {
|
||||
/** EN: Brand name and tagline / VI: Tên thương hiệu và slogan */
|
||||
name: 'GoodGo Platform',
|
||||
tagline: 'Enterprise Microservices Platform',
|
||||
description: 'Build, deploy, and scale microservices with confidence',
|
||||
|
||||
/**
|
||||
* EN: Brand color palette - Use these for consistent branding
|
||||
* VI: Bảng màu thương hiệu - Sử dụng để đảm bảo tính nhất quán
|
||||
*/
|
||||
colors: {
|
||||
/** Primary brand color - Main identity color (Blue) */
|
||||
primary: {
|
||||
main: 'var(--brand-primary)',
|
||||
light: 'var(--brand-primary-light)',
|
||||
dark: 'var(--brand-primary-dark)',
|
||||
contrast: 'var(--brand-primary-contrast)',
|
||||
hex: '#3B82F6', // For use outside CSS
|
||||
},
|
||||
/** Secondary brand color - Supporting color (Purple) */
|
||||
secondary: {
|
||||
main: 'var(--brand-secondary)',
|
||||
light: 'var(--brand-secondary-light)',
|
||||
dark: 'var(--brand-secondary-dark)',
|
||||
hex: '#8B5CF6',
|
||||
},
|
||||
/** Accent color - Call-to-action color (Cyan) */
|
||||
accent: {
|
||||
main: 'var(--brand-accent)',
|
||||
hex: '#06B6D4',
|
||||
},
|
||||
/** Brand gradients - For backgrounds and special elements */
|
||||
gradients: {
|
||||
primary: 'var(--brand-gradient-primary)',
|
||||
accent: 'var(--brand-gradient-accent)',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* EN: Typography system - Font families for different use cases
|
||||
* VI: Hệ thống typography - Font families cho các trường hợp khác nhau
|
||||
*/
|
||||
fonts: {
|
||||
display: 'var(--font-display)', // For hero titles (48px+)
|
||||
heading: 'var(--font-heading)', // For section headings (24-36px)
|
||||
body: 'var(--font-sans)', // For body text (16px)
|
||||
mono: 'var(--font-mono)', // For code blocks
|
||||
},
|
||||
|
||||
/**
|
||||
* EN: Brand assets paths - Logo, icons, illustrations
|
||||
* VI: Đường dẫn brand assets - Logo, icons, illustrations
|
||||
*/
|
||||
assets: {
|
||||
logo: {
|
||||
full: '/brand-assets/logo/logo-full.svg',
|
||||
icon: '/brand-assets/logo/logo-icon.svg',
|
||||
wordmark: '/brand-assets/logo/logo-wordmark.svg',
|
||||
},
|
||||
icons: {
|
||||
favicon: '/brand-assets/icons/favicon.svg',
|
||||
},
|
||||
illustrations: {
|
||||
empty: '/brand-assets/illustrations/empty-state.svg',
|
||||
error: '/brand-assets/illustrations/error-state.svg',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* EN: Type-safe brand color getter
|
||||
* VI: Hàm lấy màu thương hiệu type-safe
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const primaryColor = getBrandColor('primary.main');
|
||||
* const secondaryHex = getBrandColor('secondary.hex');
|
||||
* ```
|
||||
*/
|
||||
export const getBrandColor = (path: string): string => {
|
||||
const keys = path.split('.');
|
||||
let value: any = BRAND.colors;
|
||||
|
||||
for (const key of keys) {
|
||||
value = value?.[key];
|
||||
}
|
||||
|
||||
return value || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Brand logo path getter
|
||||
* VI: Hàm lấy đường dẫn logo thương hiệu
|
||||
*
|
||||
* @param variant - Logo variant: 'full' | 'icon' | 'wordmark'
|
||||
* @returns Logo file path
|
||||
*/
|
||||
export const getBrandLogo = (variant: 'full' | 'icon' | 'wordmark' = 'full'): string => {
|
||||
return BRAND.assets.logo[variant];
|
||||
};
|
||||
|
||||
/**
|
||||
* EN: Brand illustration path getter
|
||||
* VI: Hàm lấy đường dẫn illustration thương hiệu
|
||||
*
|
||||
* @param type - Illustration type: 'empty' | 'error'
|
||||
* @returns Illustration file path
|
||||
*/
|
||||
export const getBrandIllustration = (type: 'empty' | 'error'): string => {
|
||||
return BRAND.assets.illustrations[type];
|
||||
};
|
||||
@@ -75,6 +75,51 @@
|
||||
--border-focus: #FFFFFF;
|
||||
/* Focus state - White */
|
||||
|
||||
/* ============================================
|
||||
EN: Brand Colors (Primary Identity)
|
||||
VI: Màu thương hiệu (Nhận diện chính)
|
||||
============================================ */
|
||||
|
||||
/* Primary Brand Color - Main brand identity (Blue - Tech & Trust) */
|
||||
--brand-primary: #3B82F6;
|
||||
--brand-primary-light: #60A5FA;
|
||||
--brand-primary-dark: #2563EB;
|
||||
--brand-primary-contrast: #FFFFFF;
|
||||
|
||||
/* Secondary Brand Color - Supporting color (Purple - Innovation) */
|
||||
--brand-secondary: #8B5CF6;
|
||||
--brand-secondary-light: #A78BFA;
|
||||
--brand-secondary-dark: #7C3AED;
|
||||
|
||||
/* Accent Color - Call-to-action (Cyan - Energy) */
|
||||
--brand-accent: #06B6D4;
|
||||
--brand-accent-light: #22D3EE;
|
||||
--brand-accent-dark: #0891B2;
|
||||
|
||||
/* Brand Gradients - For backgrounds and special elements */
|
||||
--brand-gradient-primary: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||
--brand-gradient-accent: linear-gradient(135deg, var(--brand-accent) 0%, var(--brand-primary) 100%);
|
||||
--brand-gradient-vertical: linear-gradient(180deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
|
||||
|
||||
/* ============================================
|
||||
EN: Glassmorphism Effects
|
||||
VI: Hiệu ứng Glassmorphism
|
||||
============================================ */
|
||||
|
||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||
--glass-bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-blur: 10px;
|
||||
|
||||
/* ============================================
|
||||
EN: Extended Shadows & Effects
|
||||
VI: Shadows & Effects mở rộng
|
||||
============================================ */
|
||||
|
||||
--shadow-brand: 0 10px 40px rgba(59, 130, 246, 0.2);
|
||||
--shadow-brand-lg: 0 20px 60px rgba(59, 130, 246, 0.3);
|
||||
--shadow-colored: 0 8px 30px rgba(59, 130, 246, 0.25);
|
||||
|
||||
/* ============================================
|
||||
EN: Light Mode Colors (Secondary Theme)
|
||||
VI: Màu sắc cho Light Mode (Theme phụ)
|
||||
@@ -95,6 +140,12 @@
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
||||
/* Display Font - For hero titles (48px+) */
|
||||
--font-display: "Inter Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
/* Heading Font - For section headings (24-36px) */
|
||||
--font-heading: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
/* Type Scale / Kích thước chữ */
|
||||
--text-6xl: 3.75rem;
|
||||
/* 60px - Hero titles */
|
||||
@@ -243,6 +294,23 @@
|
||||
/* Complex animations */
|
||||
--duration-slower: 500ms;
|
||||
/* Page transitions */
|
||||
|
||||
/* ============================================
|
||||
EN: Advanced Motion Tokens
|
||||
VI: Token chuyển động nâng cao
|
||||
============================================ */
|
||||
|
||||
/* Motion Ease Functions - For micro-interactions */
|
||||
--motion-ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
--motion-ease-elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6);
|
||||
|
||||
/* Hover Scale - For interactive elements */
|
||||
--hover-scale-sm: 1.02;
|
||||
--hover-scale-md: 1.05;
|
||||
--hover-scale-lg: 1.1;
|
||||
|
||||
/* Active Scale - For pressed states */
|
||||
--active-scale: 0.98;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
||||
@@ -58,6 +58,31 @@ module.exports = {
|
||||
secondary: 'var(--border-secondary)',
|
||||
focus: 'var(--border-focus)',
|
||||
},
|
||||
// EN: Brand colors for easy access / VI: Màu thương hiệu dễ sử dụng
|
||||
brand: {
|
||||
primary: {
|
||||
DEFAULT: 'var(--brand-primary)',
|
||||
light: 'var(--brand-primary-light)',
|
||||
dark: 'var(--brand-primary-dark)',
|
||||
contrast: 'var(--brand-primary-contrast)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'var(--brand-secondary)',
|
||||
light: 'var(--brand-secondary-light)',
|
||||
dark: 'var(--brand-secondary-dark)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'var(--brand-accent)',
|
||||
light: 'var(--brand-accent-light)',
|
||||
dark: 'var(--brand-accent-dark)',
|
||||
},
|
||||
},
|
||||
// EN: Glassmorphism utilities / VI: Utilities glassmorphism
|
||||
glass: {
|
||||
bg: 'var(--glass-bg)',
|
||||
'bg-hover': 'var(--glass-bg-hover)',
|
||||
border: 'var(--glass-border)',
|
||||
},
|
||||
},
|
||||
// EN: Font families from CSS variables
|
||||
// VI: Font families từ CSS variables
|
||||
@@ -120,12 +145,26 @@ module.exports = {
|
||||
},
|
||||
// EN: Box shadows from CSS variables
|
||||
// VI: Đổ bóng từ CSS variables
|
||||
// EN: Brand gradients / VI: Gradients thương hiệu
|
||||
backgroundImage: {
|
||||
'brand-gradient': 'var(--brand-gradient-primary)',
|
||||
'brand-gradient-accent': 'var(--brand-gradient-accent)',
|
||||
'brand-gradient-vertical': 'var(--brand-gradient-vertical)',
|
||||
},
|
||||
// EN: Extended shadows / VI: Shadows mở rộng
|
||||
boxShadow: {
|
||||
sm: 'var(--shadow-sm)',
|
||||
md: 'var(--shadow-md)',
|
||||
lg: 'var(--shadow-lg)',
|
||||
xl: 'var(--shadow-xl)',
|
||||
glow: 'var(--shadow-glow)',
|
||||
brand: 'var(--shadow-brand)',
|
||||
'brand-lg': 'var(--shadow-brand-lg)',
|
||||
colored: 'var(--shadow-colored)',
|
||||
},
|
||||
// EN: Glassmorphism backdrop blur / VI: Backdrop blur glassmorphism
|
||||
backdropBlur: {
|
||||
glass: 'var(--glass-blur)',
|
||||
},
|
||||
// EN: Animation timing functions
|
||||
// VI: Hàm thời gian animation
|
||||
|
||||
Reference in New Issue
Block a user