chore: Remove the web-client application, add a local database initialization script, and update service Dockerfiles.

This commit is contained in:
Ho Ngoc Hai
2026-02-28 00:41:17 +07:00
parent c9894c5e9d
commit f521cc0a91
234 changed files with 372 additions and 29276 deletions

View File

@@ -29,8 +29,7 @@ COPY . .
# VI: Build và publish
RUN dotnet publish src/WebClientTpos.Server/WebClientTpos.Server.csproj \
-c Release \
-o /app/publish \
--no-restore
-o /app/publish
# ═══════════════════════════════════════════════════════════════════════════════
# Stage 2: Runtime
@@ -38,6 +37,11 @@ RUN dotnet publish src/WebClientTpos.Server/WebClientTpos.Server.csproj \
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
WORKDIR /app
# EN: Install ICU libs for globalization support (Alpine doesn't include them)
# VI: Cài đặt ICU libs cho hỗ trợ globalization (Alpine không bao gồm sẵn)
RUN apk add --no-cache icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
# 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

View File

@@ -1,2 +0,0 @@
# Bypass Traefik due to Docker provider error
NEXT_PUBLIC_API_URL=http://localhost/api/v1

View File

@@ -1,29 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"@goodgo/eslint-config"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"env": {
"browser": true,
"node": true,
"es2020": true
},
"ignorePatterns": [
"**/*.stories.ts",
"**/*.stories.tsx",
"**/test/**",
"**/__tests__/**",
"**/e2e/**",
"playwright.config.ts",
"vitest.config.ts"
]
}

View File

@@ -1,137 +0,0 @@
# Storybook Configuration
## Overview / Tổng quan
This Storybook setup is configured for the GoodGo web-client application with support for:
- **Next.js 14+** with App Router
- **TypeScript** with strict type checking
- **Tailwind CSS** with custom theme variables
- **Theme switching** (dark/light mode) with Context API
- **Accessibility testing** with @storybook/addon-a11y
- **Documentation** with @storybook/addon-docs
## Getting Started / Bắt đầu
### Running Storybook / Chạy Storybook
```bash
# From the web-client directory / Từ thư mục web-client
pnpm storybook
# Or from the root / Hoặc từ thư mục gốc
cd apps/web-client && pnpm storybook
```
Storybook will start on `http://localhost:6006`
### Building Storybook / Build Storybook
```bash
pnpm build-storybook
```
This creates a static build in the `storybook-static` directory.
## Configuration / Cấu hình
### Main Configuration (`main.ts`)
- **Stories location**: `../src/**/*.stories.@(js|jsx|mjs|ts|tsx)`
- **Framework**: `@storybook/nextjs-vite`
- **Path aliases**: `@/*` maps to `./src/*`
### Preview Configuration (`preview.ts`)
- **Theme Provider**: All stories are wrapped with `ThemeProvider` for theme support
- **Global Styles**: Imports `globals.css` for Tailwind CSS and theme variables
- **Accessibility**: Configured with a11y addon for accessibility testing
- **Backgrounds**: Pre-configured dark and light backgrounds
## Writing Stories / Viết Stories
### Basic Story Example / Ví dụ Story cơ bản
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
};
```
### Story with Theme Context / Story với Theme Context
All stories automatically have access to the `ThemeProvider`, so theme-dependent components work out of the box.
```tsx
export const Themed: Story = {
render: () => (
<div className="bg-secondary p-4 rounded-lg">
<Button>Theme-aware Button</Button>
</div>
),
};
```
## Addons / Tiện ích
### Available Addons / Các addon có sẵn
1. **@storybook/addon-docs**: Automatic documentation generation
2. **@storybook/addon-a11y**: Accessibility testing
3. **@storybook/addon-vitest**: Test integration
4. **@chromatic-com/storybook**: Visual testing (optional)
### Using Accessibility Addon / Sử dụng Addon Accessibility
The a11y addon is automatically configured. Use the "Accessibility" tab in the Storybook UI to check for accessibility issues.
## Best Practices / Thực hành tốt nhất
1. **Bilingual Comments**: Always include EN/VI comments in stories
2. **Accessibility**: Test all components with the a11y addon
3. **Theme Testing**: Test components in both light and dark modes
4. **Documentation**: Use the `docs` parameter to add component descriptions
5. **Story Organization**: Organize stories by feature/component type
## Troubleshooting / Khắc phục sự cố
### Path Alias Issues / Vấn đề Path Alias
If you encounter import errors with `@/*` paths, ensure:
- The `viteFinal` configuration in `main.ts` is correct
- TypeScript path mapping in `tsconfig.json` matches
### Theme Not Working / Theme không hoạt động
Ensure:
- `ThemeProvider` is imported in `preview.ts`
- `globals.css` is imported in `preview.ts`
- Components use CSS variables from the theme
### Tailwind Classes Not Working / Tailwind Classes không hoạt động
Check:
- `tailwind.config.js` includes Storybook paths
- `globals.css is imported in `preview.ts`
- PostCSS is configured correctly
## Resources / Tài nguyên
- [Storybook Documentation](https://storybook.js.org/docs)
- [Next.js + Storybook Guide](https://storybook.js.org/docs/get-started/nextjs)
- [Accessibility Testing](https://storybook.js.org/docs/writing-tests/accessibility-testing)

View File

@@ -1,39 +0,0 @@
import type { StorybookConfig } from '@storybook/nextjs-vite';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
/**
* EN: This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
* VI: Hàm này được sử dụng để resolve đường dẫn tuyệt đối của một package.
* Cần thiết trong các dự án sử dụng Yarn PnP hoặc được thiết lập trong monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
}
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-vitest'),
getAbsolutePath('@storybook/addon-a11y'),
getAbsolutePath('@storybook/addon-docs'),
getAbsolutePath('@storybook/addon-onboarding'),
],
framework: getAbsolutePath('@storybook/nextjs-vite'),
staticDirs: ['../public'],
// EN: Configure path aliases for Storybook
// VI: Cấu hình path aliases cho Storybook
viteFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
'@': join(__dirname, '../src'),
};
}
return config;
},
};
export default config;

View File

@@ -1,57 +0,0 @@
import type { Preview } from '@storybook/nextjs-vite';
import React from 'react';
import { ThemeProvider } from '../src/contexts/theme-context';
import '../src/app/globals.css';
/**
* EN: Storybook preview configuration with theme support
* VI: Cấu hình preview Storybook với hỗ trợ theme
*/
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'dark',
values: [
{
name: 'dark',
value: '#0A0A0A',
},
{
name: 'light',
value: '#FFFFFF',
},
],
},
// EN: Accessibility addon configuration
// VI: Cấu hình addon accessibility
a11y: {
config: {
rules: [
{
id: 'color-contrast',
enabled: true,
},
],
},
},
},
// EN: Decorator to wrap all stories with ThemeProvider
// VI: Decorator để bọc tất cả stories với ThemeProvider
decorators: [
(Story) => (
<ThemeProvider>
<div style={{ padding: '1rem' }}>
<Story />
</div>
</ThemeProvider>
),
],
};
export default preview;

View File

@@ -1,50 +0,0 @@
FROM node:25-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Builder stage
FROM base AS builder
RUN corepack enable pnpm
# Copy workspace configuration
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
# Create directory structure and copy all package.json files
RUN mkdir -p packages apps services
COPY packages/auth-sdk/package.json ./packages/auth-sdk/
COPY packages/http-client/package.json ./packages/http-client/
COPY packages/logger/package.json ./packages/logger/
COPY packages/tracing/package.json ./packages/tracing/
COPY packages/types/package.json ./packages/types/
COPY packages/config/eslint-config/package.json ./packages/config/eslint-config/
COPY packages/config/prettier-config/package.json ./packages/config/prettier-config/
COPY packages/config/tsconfig/package.json ./packages/config/tsconfig/
COPY apps/web-client/package.json ./apps/web-client/
COPY apps/web-admin/package.json ./apps/web-admin/
COPY services/iam-service/package.json ./services/iam-service/
# Install all dependencies for entire monorepo
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# Copy all source code
COPY packages ./packages
COPY apps/web-client ./apps/web-client
COPY turbo.json ./
# Build using turbo from root (handles dependency order automatically)
RUN pnpm turbo build --filter=web-client
# Production stage
FROM base AS runner
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy the entire workspace to preserve pnpm structure
COPY --from=builder --chown=nextjs:nodejs /app /app
WORKDIR /app/apps/web-client
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", ".next/standalone/apps/web-client/server.js"]

View File

@@ -1,185 +0,0 @@
# GoodGo Platform - Web Client
> **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 / Tính Năng
-**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
## 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 / Cài đặt dependencies
pnpm install
# Start dev server / Khởi động dev server
pnpm dev
# → http://localhost:3000
# Start Storybook / Khởi động Storybook
pnpm storybook
# → http://localhost:6006
# Build for production / Build cho production
pnpm build
# Type checking / Kiểm tra kiểu
pnpm typecheck
# Lint / Kiểm tra lỗi
pnpm lint
```
## 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

View File

@@ -1,45 +0,0 @@
import { test, expect } from '@playwright/test';
/**
* EN: E2E tests for authentication flows
* VI: E2E tests cho các luồng xác thực
*/
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
// EN: Navigate to login page / VI: Điều hướng đến trang login
await page.goto('/login');
});
test('should display login page', async ({ page }) => {
// EN: Check for heading "Sign In" / VI: Kiểm tra heading "Sign In"
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
// EN: Check for email input with exact placeholder / VI: Kiểm tra input email với placeholder chính xác
await expect(page.getByPlaceholder('you@example.com')).toBeVisible();
// EN: Check for password input with placeholder / VI: Kiểm tra input password với placeholder
await expect(page.getByPlaceholder('Password')).toBeVisible();
});
test('should show validation errors for empty form', async ({ page }) => {
const submitButton = page.getByRole('button', { name: 'Sign In' });
await submitButton.click();
// EN: Wait for validation to complete / VI: Đợi validation hoàn thành
await page.waitForTimeout(500);
// EN: Check for validation errors (email or password required) / VI: Kiểm tra lỗi validation (email hoặc password required)
const emailError = page.getByText('Email is required');
const passwordError = page.getByText('Password is required');
// EN: At least one error should be visible / VI: Ít nhất một lỗi phải hiển thị
const hasError = await emailError.isVisible().catch(() => false) ||
await passwordError.isVisible().catch(() => false);
expect(hasError).toBeTruthy();
});
test('should navigate to register page', async ({ page }) => {
await page.getByRole('link', { name: 'Sign up' }).click();
await expect(page).toHaveURL(/.*\/register/);
});
test('should navigate to forgot password page', async ({ page }) => {
await page.getByRole('link', { name: 'Forgot password?' }).click();
await expect(page).toHaveURL(/.*\/forgot-password/);
});
});

View File

@@ -1,30 +0,0 @@
import { test, expect } from '@playwright/test';
/**
* EN: E2E tests for chat functionality
* VI: E2E tests cho chức năng chat
*/
test.describe('Chat', () => {
test.beforeEach(async ({ page }) => {
// EN: Navigate to chat page (assuming authenticated) / VI: Điều hướng đến trang chat (giả sử đã authenticated)
await page.goto('/chat');
});
test('should display chat interface', async ({ page }) => {
// EN: Check for chat input with exact placeholder / VI: Kiểm tra chat input với placeholder chính xác
await expect(page.getByPlaceholder('Type your message...')).toBeVisible();
});
test('should send message', async ({ page }) => {
const input = page.getByPlaceholder('Type your message...');
// EN: Type into the textarea (controlled component) / VI: Nhập vào textarea (controlled component)
await input.type('Test message');
// EN: Wait for send button to be enabled / VI: Đợi nút send được kích hoạt
const sendButton = page.getByRole('button', { name: 'Send message' });
await expect(sendButton).toBeEnabled();
await sendButton.click();
// EN: Check if message appears / VI: Kiểm tra nếu tin nhắn xuất hiện
// Note: This would require WebSocket mocking in actual implementation
// Lưu ý: Điều này sẽ cần mock WebSocket trong implementation thực tế
});
});

View File

@@ -1,38 +0,0 @@
import { test, expect } from '@playwright/test';
/**
* EN: E2E tests for settings pages.
* VI: E2E tests cho các trang settings.
*/
test.describe('Settings', () => {
test.beforeEach(async ({ page }) => {
// EN: Seed persisted auth state before app hydration.
// VI: Seed trạng thái auth đã persist trước khi app hydrate.
await page.addInitScript(() => {
localStorage.setItem(
'auth-storage',
JSON.stringify({
state: {
user: {
id: '9d7bb2f8-4a68-4aa4-9b73-530a06039ee3',
email: 'tester@goodgo.dev',
},
isAuthenticated: true,
},
version: 0,
})
);
});
});
test('should render profile settings page', async ({ page }) => {
await page.goto('/settings/profile');
await expect(page.getByRole('heading', { level: 2, name: 'Profile' })).toBeVisible();
});
test('should render api keys settings page', async ({ page }) => {
await page.goto('/settings/api-keys');
await expect(page.getByRole('heading', { level: 2, name: 'API Keys' })).toBeVisible();
await expect(page.getByRole('button', { name: /Create API Key/i }).first()).toBeVisible();
});
});

View File

@@ -1,45 +0,0 @@
// EN: Minimal ESLint flat config for web-client package.
// VI: Cấu hình ESLint flat tối thiểu cho package web-client.
import tsParser from '@typescript-eslint/parser';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
export default [
{
ignores: [
'node_modules/**',
'.next/**',
'playwright-report/**',
'test-results/**',
'coverage/**',
'dist/**',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.stories.ts',
'**/*.stories.tsx',
'e2e/**',
'.storybook/**',
'src/test/**',
],
},
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'react-hooks': reactHooksPlugin,
},
rules: {
'no-debugger': 'error',
},
},
];

View File

@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,81 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// EN: Enable React strict mode for development warnings
// VI: Bật React strict mode để hiển thị warnings trong development
reactStrictMode: true,
// EN: Output standalone build for container deployment
// VI: Output build standalone để deploy trong container
output: 'standalone',
// EN: Image optimization configuration
// VI: Cấu hình tối ưu hình ảnh
images: {
// EN: Enable image optimization with WebP/AVIF formats
// VI: Bật tối ưu hình ảnh với định dạng WebP/AVIF
formats: ['image/avif', 'image/webp'],
// EN: Remote image domains (if needed)
// VI: Các domain hình ảnh từ xa (nếu cần)
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
],
// EN: Device sizes for responsive images
// VI: Kích thước thiết bị cho hình ảnh responsive
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// EN: Image sizes for different breakpoints
// VI: Kích thước hình ảnh cho các breakpoint khác nhau
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// EN: Environment variables exposed to the browser
// VI: Biến môi trường được expose cho browser
env: {
// EN: Public API URL for client-side API calls
// VI: URL API public để gọi API từ client-side
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1',
},
// EN: Headers for caching static assets (1 year) - Performance optimization
// VI: Headers cho caching static assets (1 năm) - Tối ưu hiệu suất
async headers() {
return [
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
// EN: Compress responses - Performance optimization
// VI: Nén responses - Tối ưu hiệu suất
compress: true,
// EN: Remove console.log in production
// VI: Xóa console.log trong production
...(process.env.NODE_ENV === 'production' && {
compiler: {
removeConsole: {
exclude: ['error', 'warn'],
},
},
}),
};
module.exports = nextConfig;

View File

@@ -1,89 +0,0 @@
{
"name": "@goodgo/web-client",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@goodgo/http-client": "workspace:*",
"@goodgo/types": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-stately/list": "^3.13.2",
"@react-stately/menu": "^3.9.9",
"@react-stately/overlays": "^3.6.21",
"@react-stately/select": "^3.9.0",
"@react-stately/toggle": "^3.9.3",
"@tanstack/react-query": "^5.90.16",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.24.8",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"next-intl": "^4.7.0",
"react": "^19.2.3",
"react-aria": "^3.45.0",
"react-aria-components": "^1.14.0",
"react-dom": "^19.2.3",
"react-hook-form": "^7.70.0",
"react-stately": "^3.43.0",
"zod": "^4.3.5",
"zustand": "^5.0.9"
},
"devDependencies": {
"@axe-core/react": "^4.11.0",
"@chromatic-com/storybook": "^4.1.3",
"@goodgo/eslint-config": "workspace:*",
"@goodgo/prettier-config": "workspace:*",
"@goodgo/tsconfig": "workspace:*",
"@playwright/test": "^1.57.0",
"@storybook/addon-a11y": "^10.1.11",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-onboarding": "^10.1.11",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/nextjs-vite": "^10.1.11",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
"eslint-plugin-storybook": "^10.1.11",
"jsdom": "^28.1.0",
"playwright": "^1.57.0",
"postcss": "^8.5.6",
"storybook": "^10.1.11",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.16"
},
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,37 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* EN: Playwright configuration for E2E tests
* VI: Cấu hình Playwright cho E2E tests
*/
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -1,10 +0,0 @@
/**
* EN: PostCSS configuration for Tailwind CSS 4
* VI: Cấu hình PostCSS cho Tailwind CSS 4
*/
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

View File

@@ -1 +0,0 @@
# Public assets

View File

@@ -1,19 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 712 B

View File

@@ -1,50 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,33 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,54 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,28 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 996 B

View File

@@ -1,31 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

View File

@@ -1,23 +0,0 @@
{
"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"
}

View File

@@ -1,149 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import LoginPage from '../app/(auth)/login/page';
import { useAuthStore } from '../stores/auth-store';
// Mock next/navigation
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
}),
}));
// Mock useTranslation
vi.mock('../shared/hooks/use-translation', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
// Mock auth store
vi.mock('../stores/auth-store', () => ({
useAuthStore: vi.fn(),
}));
describe('Auth Flow Integration', () => {
let queryClient: QueryClient;
let mockAuthStore: any;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
mockAuthStore = {
login: vi.fn(),
isLoading: false,
user: null,
isAuthenticated: false,
};
(useAuthStore as any).mockReturnValue(mockAuthStore);
});
const renderLoginPage = () => {
return render(
<QueryClientProvider client={queryClient}>
<LoginPage />
</QueryClientProvider>
);
};
describe('Login Page', () => {
it('renders login form with required fields', () => {
renderLoginPage();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
expect(screen.getByText(/remember me/i)).toBeInTheDocument();
});
it('shows validation errors for empty fields', async () => {
renderLoginPage();
const submitButton = screen.getByRole('button', { name: /login/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/validation\.emailRequired/i)).toBeInTheDocument();
expect(screen.getByText(/validation\.password/i)).toBeInTheDocument();
});
});
it('shows validation error for invalid email', async () => {
renderLoginPage();
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/validation\.email/i)).toBeInTheDocument();
});
});
it('calls login function on valid form submission', async () => {
mockAuthStore.login.mockResolvedValue(undefined);
renderLoginPage();
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockAuthStore.login).toHaveBeenCalledWith('test@example.com', 'password123');
});
});
it('shows loading state during login', async () => {
mockAuthStore.isLoading = true;
mockAuthStore.login.mockImplementation(() => new Promise(() => {})); // Never resolves
renderLoginPage();
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
expect(screen.getByText(/auth\.login\.loginButtonLoading/i)).toBeInTheDocument();
expect(submitButton).toBeDisabled();
});
it('shows error message on login failure', async () => {
const errorMessage = 'Invalid credentials';
mockAuthStore.login.mockRejectedValue(new Error(errorMessage));
renderLoginPage();
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
});
});
});

View File

@@ -1,111 +0,0 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UsersTable } from '../features/shared/components/users/UsersTable';
import { UserCard } from '../features/shared/components/users/UserCard';
import { UserForm } from '../features/shared/components/users/UserForm';
/**
* EN: Smoke tests for users components
* VI: Smoke tests cho users components
*
* These tests ensure components render without crashing and have basic functionality.
*/
describe('Users Components - Smoke Tests', () => {
const mockUser = {
id: '1',
email: 'test@example.com',
role: 'USER',
isActive: true,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
};
const mockUsers = [mockUser];
describe('UsersTable', () => {
it('renders without crashing', () => {
render(<UsersTable users={mockUsers} />);
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
it('renders loading state', () => {
render(<UsersTable users={[]} loading={true} />);
expect(screen.getByText('Loading users...')).toBeInTheDocument();
});
it('renders empty state', () => {
render(<UsersTable users={[]} />);
expect(screen.getByText('No users found')).toBeInTheDocument();
});
it('renders bulk actions when users selected', () => {
// This would require more complex setup with user interactions
// For smoke test, just ensure it renders
render(<UsersTable users={mockUsers} showBulkActions />);
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
});
describe('UserCard', () => {
it('renders user information', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('test@example.com')).toBeInTheDocument();
expect(screen.getByText('USER')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();
});
it('renders compact mode', () => {
render(<UserCard user={mockUser} compact />);
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
it('shows admin actions when enabled', () => {
render(<UserCard user={mockUser} showAdminActions />);
expect(screen.getByText('Edit')).toBeInTheDocument();
});
});
describe('UserForm', () => {
it('renders create form', () => {
render(
<UserForm
isCreate
onSubmit={() => {}}
onCancel={() => {}}
/>
);
expect(screen.getByText('Create New User')).toBeInTheDocument();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});
it('renders edit form', () => {
render(
<UserForm
user={mockUser}
onSubmit={() => {}}
onCancel={() => {}}
/>
);
expect(screen.getByText('Edit User')).toBeInTheDocument();
expect(screen.getByDisplayValue('test@example.com')).toBeInTheDocument();
});
it('shows validation errors', async () => {
render(
<UserForm
isCreate
onSubmit={() => {}}
onCancel={() => {}}
/>
);
const submitButton = screen.getByRole('button', { name: /create user/i });
submitButton.click();
// Note: Form validation requires react-hook-form setup
// This is just a basic smoke test
expect(submitButton).toBeInTheDocument();
});
});
});

View File

@@ -1,299 +0,0 @@
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import Link from 'next/link';
import { useState } from 'react';
import { authApi } from '@/services/api/auth.api';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import { useTranslations } from 'next-intl';
import { AuthControls } from '@/features/shared/components/layout/auth-controls';
/**
* EN: Create forgot password schema with translated messages
* VI: Tạo forgot password schema với thông báo đã dịch
*/
function createForgotPasswordSchema(t: (key: string) => string) {
return z.object({
email: z
.string()
.min(1, t('validation.emailRequired'))
.email(t('validation.email')),
});
}
/**
* EN: Forgot Password page component - allows users to request password reset link
* VI: Component trang quên mật khẩu - cho phép người dùng yêu cầu link đặt lại mật khẩu
*
* Features:
* - Email input with real-time validation
* - Error messages below inputs
* - Loading state on button
* - Success state with confirmation message
* - Link to check email
* - Back to login link
*
* Flow:
* 1. Enter email → Send reset link
* 2. Check email → Click link
* 3. Enter new password → Confirm
* 4. Success → Redirect to login
*/
export default function ForgotPasswordPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Success state - shows confirmation after email is sent
// VI: Trạng thái thành công - hiển thị xác nhận sau khi email được gửi
const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState('');
// EN: General error state for API errors
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState<string>('');
// EN: Create schema with translations / VI: Tạo schema với translations
const forgotPasswordSchema = createForgotPasswordSchema(t);
type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
register,
handleSubmit,
setValue,
formState: { errors, isSubmitting },
} = useForm<ForgotPasswordFormData>({
resolver: zodResolver(forgotPasswordSchema),
defaultValues: {
email: '',
},
});
/**
* EN: Handle form submission for forgot password
* VI: Xử lý submit form để quên mật khẩu
*
* @param data - Form data validated by Zod / Dữ liệu form đã được validate bởi Zod
*/
const onSubmit = async (data: ForgotPasswordFormData) => {
setApiError('');
try {
// EN: Request password reset link via API
// VI: Yêu cầu link đặt lại mật khẩu qua API
const response = await authApi.forgotPassword(data.email);
if (response.success) {
// EN: Show success message and store email for display
// VI: Hiển thị thông báo thành công và lưu email để hiển thị
setIsSuccess(true);
setSubmittedEmail(data.email);
} else {
setApiError(
response.error?.message || t('auth.forgotPassword.failedToSend')
);
}
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
setApiError(err.message || t('errors.generic'));
}
};
return (
// EN: Centered forgot password form layout with cosmic background and glassmorphism
// VI: Layout form quên mật khẩu với nền vũ trụ và hiệu ứng kínhmorphism
<main
role="main"
aria-label={t('auth.forgotPassword.title')}
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-bg-primary py-12 px-4 sm:px-6 lg:px-8"
>
{/* EN: X.ai Minimal Design - No cosmic background */}
{/* VI: X.ai Minimal Design - Không có nền vũ trụ */}
<AuthControls />
<div className="w-full max-w-md md:max-w-[400px] space-y-8 relative z-10 glass-appear">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-3 rounded-2xl bg-accent-primary/5 border border-accent-primary/10 shadow-glass-sm">
{/* EN: X.ai Minimal - Static icon (no floating) */}
{/* VI: X.ai Minimal - Icon tĩnh (không float) */}
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-text-primary"
>
<path
d="M12 2L14.4 9.6H22L15.8 14.2L18.2 21.8L12 17.2L5.8 21.8L8.2 14.2L2 9.6H9.6L12 2Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-text-primary mb-2">
{t('auth.forgotPassword.title')}
</h1>
<p className="text-text-secondary">
{isSuccess
? t('auth.forgotPassword.checkEmail')
: t('auth.forgotPassword.description')}
</p>
</div>
<div className="glass-card p-8 shadow-glass-xl border-border-primary">
{isSuccess ? (
// EN: Success state - show confirmation message
// VI: Trạng thái thành công - hiển thị thông báo xác nhận
<div className="space-y-6 animate-in fade-in zoom-in duration-500">
<div
className="p-4 rounded-xl bg-accent-success/10 border border-accent-success/20 text-accent-success flex items-center gap-3"
role="alert"
>
<svg
className="h-6 w-6 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-sm font-semibold">
{t('auth.forgotPassword.resetLinkSent')}
</span>
</div>
<div className="space-y-4 text-sm text-text-secondary leading-relaxed">
<p>
{t('auth.forgotPassword.resetLinkSentDetail', {
email: submittedEmail,
})}
</p>
<div className="pt-4 border-t border-glass-subtle">
<p className="text-text-tertiary">
{t('auth.forgotPassword.checkInbox')}
</p>
</div>
</div>
<Button
variant="brand"
size="lg"
className="w-full mt-4"
onPress={() => (window.location.href = '/login')}
>
{t('auth.forgotPassword.backToLogin')}
</Button>
<button
type="button"
className="w-full text-sm text-text-tertiary hover:text-text-primary transition-colors"
onClick={() => {
setIsSuccess(false);
setSubmittedEmail('');
}}
>
{t('auth.forgotPassword.sendToAnotherEmail')}
</button>
</div>
) : (
// EN: Form state - email input and submit
// VI: Trạng thái form - input email và submit
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<Input
type="email"
variant="solid"
label={t('auth.forgotPassword.email')}
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
name={register('email').name}
onBlur={register('email').onBlur}
onChange={(value) => {
// EN: React Aria onChange receives value string, not event
// VI: React Aria onChange nhận value string, không phải event
setValue('email', value);
}}
autoComplete="email"
autoFocus
aria-required="true"
/>
<Button
type="submit"
variant="brand"
size="lg"
className="w-full mt-4"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isSubmitting
? t('auth.forgotPassword.sending')
: t('auth.forgotPassword.sendResetLink')}
</Button>
<div className="text-center pt-4 border-t border-border-primary/50">
<Link
href="/login"
className="inline-flex items-center justify-center gap-2 px-6 py-2.5 rounded-xl border border-border-primary bg-bg-secondary text-sm font-semibold text-text-primary hover:bg-bg-tertiary hover:border-border-secondary transition-all group"
>
<svg
className="h-4 w-4 transform group-hover:-translate-x-1 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t('auth.forgotPassword.backToLogin')}
</Link>
</div>
</form>
)}
</div>
</div>
</main>
);
}

View File

@@ -1,241 +0,0 @@
'use client';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useState } from 'react';
import { useAuthStore } from '@/stores/auth-store';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import { useTranslations } from 'next-intl';
import { AuthControls } from '@/features/shared/components/layout/auth-controls';
import { AuthCard } from '@/features/auth/components/auth-card';
/**
* EN: Create login schema with translated messages
* VI: Tạo login schema với thông báo đã dịch
*/
function createLoginSchema(t: (key: string) => string) {
return z.object({
email: z
.string()
.min(1, t('validation.emailRequired'))
.email(t('validation.email')),
password: z
.string()
.min(1, t('validation.password'))
.min(8, t('validation.passwordMin')),
rememberMe: z.boolean().optional(),
});
}
/**
* EN: LoginFormData type - will be inferred from schema in component
* VI: Kiểu LoginFormData - sẽ được suy luận từ schema trong component
*/
/**
* EN: Login page component for user authentication
* VI: Component trang đăng nhập để xác thực người dùng
*
* Features:
* - Email/password inputs with validation
* - Real-time error messages
* - Remember me checkbox
* - Forgot password link
* - Loading state on button
* - Error handling
*/
export default function LoginPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Next.js router for navigation
// VI: Next.js router để điều hướng
const router = useRouter();
// EN: Auth store hooks for login functionality
// VI: Auth store hooks cho chức năng đăng nhập
const { login, isLoading } = useAuthStore();
// EN: General error state for API errors
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState<string>('');
// EN: Create schema with translations / VI: Tạo schema với translations
const loginSchema = createLoginSchema(t);
type LoginFormData = z.infer<typeof loginSchema>;
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
rememberMe: false,
},
});
/**
* EN: Handle form submission for login
* VI: Xử lý submit form để đăng nhập
*
* @param data - Form data validated by Zod / Dữ liệu form đã được validate bởi Zod
*/
const onSubmit = async (data: LoginFormData) => {
setApiError('');
try {
// EN: Attempt login through auth store
// VI: Thử đăng nhập thông qua auth store
await login(data.email, data.password);
// EN: Redirect to dashboard on successful login
// VI: Chuyển hướng về dashboard khi đăng nhập thành công
router.push('/dashboard');
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
setApiError(err.message || t('auth.login.loginFailed'));
}
};
return (
<>
<AuthControls />
<AuthCard
title={t('auth.login.title')}
description={t('auth.login.description')}
footer={
<span>
{t('auth.login.noAccount')}{' '}
<Link
href="/register"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
>
{t('auth.login.signUp')}
</Link>
</span>
}
>
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<div className="space-y-4">
{/* EN: Email input field / VI: Trường nhập email */}
<Controller
name="email"
control={control}
render={({ field }) => (
<Input
type="email"
variant="solid"
label={t('auth.login.email')}
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="email"
aria-required="true"
/>
)}
/>
{/* EN: Password input field / VI: Trường nhập mật khẩu */}
<div className="space-y-1">
<Controller
name="password"
control={control}
render={({ field }) => (
<Input
type="password"
variant="solid"
label={t('auth.login.password')}
placeholder={t('auth.login.password')}
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="current-password"
aria-required="true"
/>
)}
/>
<div className="flex items-center justify-between pt-2">
<label className="flex items-center gap-2 cursor-pointer group">
<Controller
name="rememberMe"
control={control}
render={({ field }) => (
<input
type="checkbox"
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
className="w-4 h-4 rounded border-border-primary bg-bg-secondary text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer transition-all"
/>
)}
/>
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">
{t('auth.login.rememberMe')}
</span>
</label>
<Link
href="/forgot-password"
className="text-sm font-medium text-accent-primary hover:text-accent-primary-hover transition-colors"
>
{t('auth.login.forgotPassword')}
</Link>
</div>
</div>
</div>
<Button
type="submit"
variant="brand"
size="lg"
className="w-full mt-4"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
? t('auth.login.signingIn')
: t('auth.login.title')}
</Button>
</form>
</AuthCard>
</>
);
}

View File

@@ -1,444 +0,0 @@
'use client';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useState, useMemo } from 'react';
import { useAuthStore } from '@/stores/auth-store';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import { AuthCard } from '@/features/auth/components/auth-card';
import { useTranslations } from 'next-intl';
import { AuthControls } from '@/features/shared/components/layout/auth-controls';
import { cn } from '@/shared/utils';
/**
* EN: Create register schema with translated messages
* VI: Tạo register schema với thông báo đã dịch
*/
function createRegisterSchema(t: (key: string) => string) {
return z
.object({
fullName: z
.string()
.min(1, t('validation.fullNameRequired'))
.min(2, t('validation.fullNameMin'))
.max(100, t('validation.fullNameMax')),
email: z
.string()
.min(1, t('validation.emailRequired'))
.email(t('validation.email')),
password: z
.string()
.min(1, t('validation.password'))
.min(8, t('validation.passwordMin'))
.regex(/[A-Z]/, t('validation.passwordUppercase'))
.regex(/[a-z]/, t('validation.passwordLowercase'))
.regex(/[0-9]/, t('validation.passwordNumber'))
.regex(/[^A-Za-z0-9]/, t('validation.passwordSpecial')),
confirmPassword: z
.string()
.min(1, t('validation.passwordConfirmRequired')),
terms: z.boolean().refine((val) => val === true, {
message: t('validation.termsRequired'),
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: t('validation.passwordConfirm'),
path: ['confirmPassword'],
});
}
/**
* EN: Password strength levels
* VI: Các mức độ mạnh của mật khẩu
*/
type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong';
/**
* EN: Calculate password strength based on criteria
* VI: Tính toán độ mạnh mật khẩu dựa trên các tiêu chí
*
* @param password - Password to evaluate / Mật khẩu cần đánh giá
* @returns Password strength level / Mức độ mạnh mật khẩu
*/
function calculatePasswordStrength(password: string): {
strength: PasswordStrength;
percentage: number;
feedback: string;
} {
if (!password) {
return { strength: 'weak', percentage: 0, feedback: '' };
}
let score = 0;
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[^A-Za-z0-9]/.test(password),
};
// EN: Calculate score based on criteria / VI: Tính điểm dựa trên tiêu chí
if (checks.length) score += 20;
if (checks.uppercase) score += 20;
if (checks.lowercase) score += 20;
if (checks.number) score += 20;
if (checks.special) score += 20;
// EN: Additional points for length / VI: Điểm thêm cho độ dài
if (password.length >= 12) score += 10;
if (password.length >= 16) score += 10;
// EN: Cap at 100% / VI: Giới hạn ở 100%
score = Math.min(score, 100);
let strength: PasswordStrength;
let feedback: string;
// EN: This will be translated in the component / VI: Sẽ được dịch trong component
if (score < 25) {
strength = 'weak';
feedback = 'weak';
} else if (score < 50) {
strength = 'fair';
feedback = 'fair';
} else if (score < 75) {
strength = 'good';
feedback = 'good';
} else {
strength = 'strong';
feedback = 'strong';
}
return { strength, percentage: score, feedback };
}
/**
* EN: Register page component for user registration
* VI: Component trang đăng ký để đăng ký người dùng
*
* Features:
* - Full name, email, password, confirm password inputs
* - Password strength indicator with visual feedback
* - Terms & conditions checkbox
* - Real-time validation
* - Error handling
*/
export default function RegisterPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Next.js router for navigation
// VI: Next.js router để điều hướng
const router = useRouter();
// EN: Auth store hooks for registration functionality
// VI: Auth store hooks cho chức năng đăng ký
const { register: registerUser, isLoading } = useAuthStore();
// EN: General error state for API errors
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState<string>('');
// EN: Create schema with translations / VI: Tạo schema với translations
const registerSchema = createRegisterSchema(t);
type RegisterFormData = z.infer<typeof registerSchema>;
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
control,
handleSubmit,
watch,
formState: { errors, isSubmitting },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
defaultValues: {
fullName: '',
email: '',
password: '',
confirmPassword: '',
terms: false,
},
});
// EN: Watch password field for strength calculation
// VI: Theo dõi trường password để tính độ mạnh
const password = watch('password');
// EN: Calculate password strength
// VI: Tính toán độ mạnh mật khẩu
const passwordStrength = useMemo(
() => calculatePasswordStrength(password || ''),
[password]
);
/**
* EN: Handle form submission for registration
* VI: Xử lý submit form để đăng ký
*
* @param data - Form data validated by Zod / Dữ liệu form đã được validate bởi Zod
* Note: fullName is collected for UX but not sent to API (backend generates username from email)
* Ghi chú: fullName được thu thập cho UX nhưng không gửi đến API (backend tạo username từ email)
*/
const onSubmit = async (data: RegisterFormData) => {
setApiError('');
try {
// EN: Attempt registration through auth store
// VI: Thử đăng ký thông qua auth store
// Note: RegisterDto only accepts email, password, confirmPassword
// Ghi chú: RegisterDto chỉ chấp nhận email, password, confirmPassword
await registerUser(data.email, data.password, data.confirmPassword);
// EN: Redirect to home page on successful registration
// VI: Chuyển hướng về trang chủ khi đăng ký thành công
router.push('/dashboard');
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
setApiError(err.message || t('auth.register.registrationFailed'));
}
};
// EN: Get color for password strength indicator
// VI: Lấy màu cho chỉ báo độ mạnh mật khẩu
const getStrengthColor = (strength: PasswordStrength) => {
switch (strength) {
case 'weak':
return 'bg-accent-error'; // Red
case 'fair':
return 'bg-accent-warning'; // Amber
case 'good':
return 'bg-accent-warning'; // Yellow (using warning for good)
case 'strong':
return 'bg-accent-success'; // Green
default:
return 'bg-border-primary';
}
};
return (
<>
<AuthControls />
<AuthCard
title={t('auth.register.createAccount')}
description={t('auth.register.signUpToStart')}
footer={
<span>
{t('auth.register.alreadyHaveAccount')}{' '}
<Link
href="/login"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
>
{t('auth.register.signIn')}
</Link>
</span>
}
>
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<div className="space-y-4">
{/* EN: Full name input field / VI: Trường nhập họ tên */}
<Controller
name="fullName"
control={control}
render={({ field }) => (
<Input
type="text"
variant="solid"
label={t('auth.register.fullName')}
placeholder="John Doe"
isInvalid={!!errors.fullName}
errorMessage={errors.fullName?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="name"
aria-required="true"
/>
)}
/>
{/* EN: Email input field / VI: Trường nhập email */}
<Controller
name="email"
control={control}
render={({ field }) => (
<Input
type="email"
variant="solid"
label={t('auth.register.email')}
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="email"
aria-required="true"
/>
)}
/>
{/* EN: Password input field / VI: Trường nhập mật khẩu */}
<div className="space-y-2">
<Controller
name="password"
control={control}
render={({ field }) => (
<Input
type="password"
variant="solid"
label={t('auth.register.password')}
placeholder={t('auth.register.createStrongPassword')}
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="new-password"
aria-required="true"
/>
)}
/>
{/* EN: Password strength indicator / VI: Chỉ báo độ mạnh mật khẩu */}
{password && (
<div className="space-y-2 px-1">
<div className="flex gap-1.5">
{[1, 2, 3, 4].map((step) => (
<div
key={step}
className={cn(
'h-1 flex-1 rounded-full transition-all duration-500',
passwordStrength.percentage >= step * 25
? getStrengthColor(passwordStrength.strength)
: 'bg-bg-tertiary'
)}
/>
))}
</div>
<p
className={cn(
'text-[10px] uppercase tracking-wider font-semibold',
passwordStrength.strength === 'weak'
? 'text-accent-error'
: passwordStrength.strength === 'strong'
? 'text-accent-success'
: 'text-accent-warning'
)}
>
{t(`auth.register.${passwordStrength.feedback}`)}
</p>
</div>
)}
</div>
{/* EN: Confirm password input field / VI: Trường xác nhận mật khẩu */}
<Controller
name="confirmPassword"
control={control}
render={({ field }) => (
<Input
type="password"
variant="solid"
label={t('auth.register.confirmPassword')}
placeholder={t('auth.register.reEnterPassword')}
isInvalid={!!errors.confirmPassword}
errorMessage={errors.confirmPassword?.message}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
autoComplete="new-password"
aria-required="true"
/>
)}
/>
{/* EN: Terms and conditions checkbox / VI: Checkbox điều khoản và điều kiện */}
<Controller
name="terms"
control={control}
render={({ field }) => (
<div className="space-y-2 pt-2">
<label className="flex items-start gap-2 cursor-pointer group">
<input
type="checkbox"
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
className="mt-1 w-4 h-4 rounded border-border-primary bg-bg-secondary text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer flex-shrink-0 transition-all"
aria-required="true"
aria-invalid={errors.terms ? 'true' : 'false'}
/>
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">
{t('auth.register.agreeToTerms')}{' '}
<Link
href="/terms"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
target="_blank"
rel="noopener noreferrer"
>
{t('auth.register.termsAndConditions')}
</Link>
</span>
</label>
{errors.terms && (
<p
className="text-sm text-accent-error flex items-center gap-1 ml-6 animate-in fade-in duration-quick"
role="alert"
>
<span>{errors.terms.message}</span>
</p>
)}
</div>
)}
/>
</div>
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="brand"
size="lg"
fullWidth
className="mt-2"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
? t('auth.register.signingUp')
: t('auth.register.title')}
</Button>
</form>
</AuthCard>
</>
);
}

View File

@@ -1,601 +0,0 @@
'use client';
/**
* EN: Account Mockup Component
* VI: Component Mockup Tài khoản
*
* This is a visual mockup/demo component for the client account page.
* It showcases the account overview, profile information, statistics, and quick actions.
*
* Đây là component mockup/demo cho trang tài khoản client.
* Nó minh họa tổng quan tài khoản, thông tin profile, thống kê, và các hành động nhanh.
*/
import Link from 'next/link';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@/features/shared/components/ui/card';
import { Button } from '@/features/shared/components/ui/button';
import {
Avatar,
AvatarImage,
AvatarFallback,
} from '@/features/shared/components/ui/avatar';
import {
User,
Mail,
Calendar,
Shield,
Activity,
TrendingUp,
MessageSquare,
Settings,
CreditCard,
Bell,
CheckCircle2,
Clock,
BarChart3,
Zap,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import { cn } from '@/shared/lib/utils';
/**
* EN: Account Statistics Card
* VI: Card Thống kê Tài khoản
*/
function AccountStatsCard() {
const t = useTranslations();
const stats = [
{
label: t('account.stats.totalChats', { defaultValue: 'Total Chats' }),
value: '1,234',
change: '+12%',
trend: 'up',
icon: MessageSquare,
color: 'text-accent-primary',
},
{
label: t('account.stats.activeSessions', { defaultValue: 'Active Sessions' }),
value: '5',
change: '+2',
trend: 'up',
icon: Activity,
color: 'text-accent-success',
},
{
label: t('account.stats.apiCalls', { defaultValue: 'API Calls (This Month)' }),
value: '45.2K',
change: '+8%',
trend: 'up',
icon: TrendingUp,
color: 'text-accent-info',
},
{
label: t('account.stats.uptime', { defaultValue: 'Uptime' }),
value: '99.9%',
change: 'Stable',
trend: 'stable',
icon: BarChart3,
color: 'text-accent-success',
},
];
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<Card key={index} hover bordered>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-text-secondary">
{stat.label}
</p>
<p className="mt-2 text-2xl font-bold text-text-primary">
{stat.value}
</p>
<div className="mt-2 flex items-center gap-1">
{stat.trend === 'up' && (
<span className="text-xs font-medium text-accent-success">
{stat.change}
</span>
)}
{stat.trend === 'stable' && (
<span className="text-xs font-medium text-text-tertiary">
{stat.change}
</span>
)}
<span className="text-xs text-text-tertiary">
{t('account.stats.vsLastMonth', { defaultValue: 'vs last month' })}
</span>
</div>
</div>
<div className={`rounded-lg bg-bg-tertiary p-3 ${stat.color}`}>
<Icon className="h-6 w-6" />
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
);
}
/**
* EN: Quick Actions Card
* VI: Card Hành động Nhanh
*/
function QuickActionsCard() {
const t = useTranslations();
const actions = [
{
label: t('account.actions.updateProfile', { defaultValue: 'Update Profile' }),
icon: User,
href: '/settings/profile',
color: 'text-accent-primary',
},
{
label: t('account.actions.manageApiKeys', { defaultValue: 'Manage API Keys' }),
icon: Shield,
href: '/settings/api-keys',
color: 'text-accent-warning',
},
{
label: t('account.actions.billing', { defaultValue: 'Billing & Subscription' }),
icon: CreditCard,
href: '/settings/billing',
color: 'text-accent-info',
},
{
label: t('account.actions.notifications', { defaultValue: 'Notification Settings' }),
icon: Bell,
href: '/settings/notifications',
color: 'text-accent-secondary',
},
];
return (
<Card hover>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-accent-primary" />
{t('account.quickActions', { defaultValue: 'Quick Actions' })}
</CardTitle>
<CardDescription>
{t('account.quickActionsDescription', {
defaultValue: 'Common account management tasks',
})}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{actions.map((action, index) => {
const Icon = action.icon;
return (
<Link
key={index}
href={action.href}
className={cn(
'inline-flex items-center justify-start gap-3 rounded-md font-medium transition-all duration-[150ms] ease-out h-auto py-3 px-4',
'bg-chat-ai-bubble text-chat-ai-text hover:bg-bg-tertiary hover:scale-[1.02] active:scale-[0.98]',
'border border-border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-accent-primary'
)}
>
<Icon className={`h-5 w-5 ${action.color}`} />
<span>{action.label}</span>
</Link>
);
})}
</div>
</CardContent>
</Card>
);
}
/**
* EN: Account Information Card
* VI: Card Thông tin Tài khoản
*/
function AccountInfoCard() {
const t = useTranslations();
// EN: Mock user data / VI: Dữ liệu user mẫu
const userData = {
name: 'John Doe',
email: 'john.doe@example.com',
username: 'johndoe',
memberSince: 'January 2024',
emailVerified: true,
phoneVerified: false,
plan: 'Pro',
avatarUrl: null,
};
const infoItems = [
{
label: t('account.info.email', { defaultValue: 'Email' }),
value: userData.email,
verified: userData.emailVerified,
icon: Mail,
},
{
label: t('account.info.username', { defaultValue: 'Username' }),
value: userData.username,
verified: false,
icon: User,
},
{
label: t('account.info.memberSince', { defaultValue: 'Member Since' }),
value: userData.memberSince,
verified: false,
icon: Calendar,
},
{
label: t('account.info.plan', { defaultValue: 'Plan' }),
value: userData.plan,
verified: false,
icon: Shield,
},
];
return (
<Card hover>
<CardHeader>
<CardTitle>{t('account.info.title', { defaultValue: 'Account Information' })}</CardTitle>
<CardDescription>
{t('account.info.description', {
defaultValue: 'Your account details and verification status',
})}
</CardDescription>
</CardHeader>
<CardContent>
{/* EN: Avatar and Name Section / VI: Phần Avatar và Tên */}
<div className="mb-6 flex items-center gap-4 border-b border-border-primary pb-6">
<Avatar size="xl" className="h-20 w-20">
{userData.avatarUrl && (
<AvatarImage src={userData.avatarUrl} alt={userData.name} />
)}
<AvatarFallback>
{userData.name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="text-xl font-semibold text-text-primary">{userData.name}</h3>
<p className="text-sm text-text-secondary">{userData.email}</p>
<div className="mt-2 flex items-center gap-2">
{userData.emailVerified && (
<span className="inline-flex items-center gap-1 rounded-full bg-accent-success/10 px-2 py-1 text-xs font-medium text-accent-success">
<CheckCircle2 className="h-3 w-3" />
{t('account.verified', { defaultValue: 'Verified' })}
</span>
)}
<span className="inline-flex items-center gap-1 rounded-full bg-accent-primary/10 px-2 py-1 text-xs font-medium text-accent-primary">
{userData.plan}
</span>
</div>
</div>
<Link
href="/settings/profile"
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-all duration-[150ms] ease-out',
'bg-chat-ai-bubble text-chat-ai-text hover:bg-bg-tertiary hover:scale-[1.02] active:scale-[0.98]',
'border border-border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-accent-primary',
'h-10 px-4 text-base'
)}
>
{t('account.editProfile', { defaultValue: 'Edit Profile' })}
</Link>
</div>
{/* EN: Information Grid / VI: Lưới Thông tin */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{infoItems.map((item, index) => {
const Icon = item.icon;
return (
<div
key={index}
className="flex items-start gap-3 rounded-lg bg-bg-tertiary p-4"
>
<div className="rounded-lg bg-bg-elevated p-2 text-text-tertiary">
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-text-secondary">{item.label}</p>
<p className="mt-1 text-base text-text-primary">{item.value}</p>
{item.verified && (
<div className="mt-1 flex items-center gap-1 text-xs text-accent-success">
<CheckCircle2 className="h-3 w-3" />
<span>{t('account.verified', { defaultValue: 'Verified' })}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
);
}
/**
* EN: Recent Activity Card
* VI: Card Hoạt động Gần đây
*/
function RecentActivityCard() {
const t = useTranslations();
// EN: Mock activity data / VI: Dữ liệu hoạt động mẫu
const activities = [
{
type: 'api_key_created',
message: t('account.activity.apiKeyCreated', {
defaultValue: 'Created new API key',
}),
time: '2 hours ago',
icon: Shield,
color: 'text-accent-success',
},
{
type: 'profile_updated',
message: t('account.activity.profileUpdated', {
defaultValue: 'Updated profile information',
}),
time: '1 day ago',
icon: User,
color: 'text-accent-primary',
},
{
type: 'chat_created',
message: t('account.activity.chatCreated', {
defaultValue: 'Started new conversation',
}),
time: '2 days ago',
icon: MessageSquare,
color: 'text-accent-info',
},
{
type: 'settings_changed',
message: t('account.activity.settingsChanged', {
defaultValue: 'Changed notification preferences',
}),
time: '3 days ago',
icon: Settings,
color: 'text-accent-warning',
},
];
return (
<Card hover>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-accent-primary" />
{t('account.recentActivity', { defaultValue: 'Recent Activity' })}
</CardTitle>
<CardDescription>
{t('account.recentActivityDescription', {
defaultValue: 'Your recent account activities and changes',
})}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{activities.map((activity, index) => {
const Icon = activity.icon;
return (
<div
key={index}
className="flex items-start gap-4 rounded-lg border border-border-primary bg-bg-tertiary p-4 transition-all hover:border-border-secondary"
>
<div className={`rounded-lg bg-bg-elevated p-2 ${activity.color}`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-text-primary">
{activity.message}
</p>
<div className="mt-1 flex items-center gap-2 text-xs text-text-tertiary">
<Clock className="h-3 w-3" />
<span>{activity.time}</span>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-4">
<Button variant="ghost" size="sm" className="w-full">
{t('account.viewAllActivity', { defaultValue: 'View All Activity' })}
</Button>
</div>
</CardContent>
</Card>
);
}
/**
* EN: Account Security Status Card
* VI: Card Trạng thái Bảo mật Tài khoản
*/
function SecurityStatusCard() {
const t = useTranslations();
const securityItems = [
{
label: t('account.security.emailVerified', { defaultValue: 'Email Verified' }),
status: true,
icon: CheckCircle2,
},
{
label: t('account.security.twoFactorEnabled', { defaultValue: 'Two-Factor Authentication' }),
status: false,
icon: Shield,
},
{
label: t('account.security.phoneVerified', { defaultValue: 'Phone Verified' }),
status: false,
icon: CheckCircle2,
},
{
label: t('account.security.recoveryCodes', {
defaultValue: 'Recovery Codes Generated',
}),
status: true,
icon: Shield,
},
];
const completedCount = securityItems.filter((item) => item.status).length;
const totalCount = securityItems.length;
const percentage = (completedCount / totalCount) * 100;
return (
<Card hover>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-accent-primary" />
{t('account.security.title', { defaultValue: 'Security Status' })}
</CardTitle>
<CardDescription>
{t('account.security.description', {
defaultValue: 'Your account security settings and recommendations',
})}
</CardDescription>
</CardHeader>
<CardContent>
{/* EN: Security Score / VI: Điểm Bảo mật */}
<div className="mb-6">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-text-secondary">
{t('account.security.score', { defaultValue: 'Security Score' })}
</span>
<span className="text-sm font-bold text-text-primary">
{completedCount}/{totalCount}
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-bg-tertiary">
<div
className="h-full rounded-full bg-accent-primary transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
<p className="mt-2 text-xs text-text-tertiary">
{percentage === 100
? t('account.security.excellent', {
defaultValue: 'Excellent! Your account is well secured.',
})
: t('account.security.improve', {
defaultValue: 'Complete remaining items to improve security.',
})}
</p>
</div>
{/* EN: Security Items List / VI: Danh sách Mục Bảo mật */}
<div className="space-y-3">
{securityItems.map((item, index) => {
const Icon = item.icon;
return (
<div
key={index}
className="flex items-center justify-between rounded-lg border border-border-primary bg-bg-tertiary p-3"
>
<div className="flex items-center gap-3">
<Icon
className={`h-5 w-5 ${
item.status ? 'text-accent-success' : 'text-text-tertiary'
}`}
/>
<span className="text-sm font-medium text-text-primary">
{item.label}
</span>
</div>
{item.status ? (
<span className="inline-flex items-center gap-1 rounded-full bg-accent-success/10 px-2 py-1 text-xs font-medium text-accent-success">
<CheckCircle2 className="h-3 w-3" />
{t('account.security.enabled', { defaultValue: 'Enabled' })}
</span>
) : (
<Link
href="/settings/security"
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-all duration-[150ms] ease-out',
'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary active:bg-bg-elevated',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-accent-primary',
'h-8 px-3 text-sm'
)}
>
{t('account.security.enable', { defaultValue: 'Enable' })}
</Link>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
);
}
/**
* EN: Main Account Mockup Page
* VI: Trang Mockup Tài khoản Chính
*/
export default function AccountMockupPage() {
const t = useTranslations();
return (
<div className="min-h-screen bg-bg-primary">
{/* EN: Page Header / VI: Header Trang */}
<div className="border-b border-border-primary bg-bg-secondary">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="py-8">
<h1 className="text-3xl font-bold text-text-primary">
{t('account.title', { defaultValue: 'My Account' })}
</h1>
<p className="mt-2 text-sm text-text-tertiary">
{t('account.description', {
defaultValue: 'Manage your account settings, view statistics, and monitor activity',
})}
</p>
</div>
</div>
</div>
{/* EN: Main Content / VI: Nội dung Chính */}
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="space-y-8">
{/* EN: Statistics Cards / VI: Cards Thống kê */}
<AccountStatsCard />
{/* EN: Main Grid Layout / VI: Bố cục Lưới Chính */}
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* EN: Left Column - Account Info / VI: Cột Trái - Thông tin Tài khoản */}
<div className="lg:col-span-2 space-y-8">
<AccountInfoCard />
<RecentActivityCard />
</div>
{/* EN: Right Column - Quick Actions & Security / VI: Cột Phải - Hành động Nhanh & Bảo mật */}
<div className="space-y-8">
<QuickActionsCard />
<SecurityStatusCard />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,23 +0,0 @@
import AccountMockupPage from './mockup';
/**
* EN: Account Page - Displays account overview and management
* VI: Trang Tài khoản - Hiển thị tổng quan và quản lý tài khoản
*
* This page shows:
* - Account statistics (chats, sessions, API calls, uptime)
* - Account information (profile, email, username, plan)
* - Quick actions (update profile, manage API keys, billing, notifications)
* - Recent activity feed
* - Security status and recommendations
*
* Trang này hiển thị:
* - Thống kê tài khoản (chats, sessions, API calls, uptime)
* - Thông tin tài khoản (profile, email, username, plan)
* - Hành động nhanh (cập nhật profile, quản lý API keys, billing, notifications)
* - Feed hoạt động gần đây
* - Trạng thái bảo mật và khuyến nghị
*/
export default function AccountPage() {
return <AccountMockupPage />;
}

View File

@@ -1,171 +0,0 @@
'use client';
import * as React from 'react';
import { ChatLayout } from '@/features/chat/chat-layout';
import { ConversationSidebar } from '@/features/chat/conversation-sidebar';
import { MessageBubble } from '@/features/chat/message-bubble';
import { ChatInput } from '@/features/chat/chat-input';
// EN: Lazy load typing indicator / VI: Lazy load typing indicator
const TypingIndicator = React.lazy(() => import('@/features/chat/typing-indicator').then(m => ({ default: m.TypingIndicator })));
import { LiveRegion } from '@/features/shared/components/accessibility/live-region';
import { useKeyboardShortcuts, CHAT_SHORTCUTS } from '@/shared/hooks/use-keyboard-shortcuts';
import { useChatStore, MessageSender } from '@/stores/chat-store';
import { useTranslations } from 'next-intl';
/**
* EN: Chat page component - Main chat interface
* VI: Component trang chat - Giao diện chat chính
*
* Features:
* - Chat layout with sidebar and main area
* - Message display with user/AI variants
* - Chat input with auto-resize
* - Typing indicator
* - Keyboard shortcuts
* - Screen reader announcements
*
* Tính năng:
* - Layout chat với sidebar và khu vực chính
* - Hiển thị tin nhắn với các biến thể user/AI
* - Input chat với auto-resize
* - Typing indicator
* - Phím tắt bàn phím
* - Thông báo screen reader
*/
export default function ChatPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const [sidebarVisible, setSidebarVisible] = React.useState(true);
const [announcement, setAnnouncement] = React.useState<string>('');
// EN: Get chat state from store / VI: Lấy state chat từ store
const {
messages,
conversations,
currentConversationId,
sendMessage,
selectConversation,
createConversation,
} = useChatStore();
// EN: Get current conversation messages / VI: Lấy tin nhắn của conversation hiện tại
const currentMessages = React.useMemo(() => {
if (!currentConversationId) return [];
return messages[currentConversationId] || [];
}, [messages, currentConversationId]);
// EN: Handle send message / VI: Xử lý gửi tin nhắn
const handleSend = async (content: string) => {
let conversationId = currentConversationId;
if (!conversationId) {
// EN: Create new conversation if none selected / VI: Tạo conversation mới nếu chưa chọn
conversationId = createConversation();
}
try {
await sendMessage(conversationId, content);
setAnnouncement(t('chat.messageSent'));
} catch (error) {
setAnnouncement(t('chat.messageSendFailed'));
}
};
// EN: Handle new chat / VI: Xử lý chat mới
const handleNewChat = () => {
createConversation();
setAnnouncement(t('chat.newConversationCreated'));
};
// EN: Handle select conversation / VI: Xử lý chọn conversation
const handleSelectConversation = (conversationId: string) => {
selectConversation(conversationId);
setAnnouncement(t('chat.switchedToConversation'));
};
// EN: Keyboard shortcuts / VI: Phím tắt bàn phím
useKeyboardShortcuts([
{
key: CHAT_SHORTCUTS.NEW_CHAT,
handler: () => handleNewChat(),
description: t('chat.newChat'),
preventDefault: true,
},
{
key: CHAT_SHORTCUTS.SEARCH,
handler: () => {
// EN: Focus search input / VI: Focus input tìm kiếm
const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement;
searchInput?.focus();
},
description: t('chat.openSearch'),
preventDefault: true,
},
]);
return (
<>
<LiveRegion message={announcement} priority="polite" />
<ChatLayout
sidebar={
<ConversationSidebar
conversations={conversations.map((conv) => ({
id: conv.id,
title: conv.title,
lastMessage: conv.lastMessage?.content,
lastMessageAt: conv.lastMessage ? new Date(conv.lastMessage.createdAt) : undefined,
isSelected: conv.id === currentConversationId,
}))}
selectedConversationId={currentConversationId || undefined}
onSelectConversation={handleSelectConversation}
onNewChat={handleNewChat}
/>
}
sidebarVisible={sidebarVisible}
onSidebarToggle={setSidebarVisible}
>
{/* EN: Messages container / VI: Container tin nhắn */}
<div className="flex-1 overflow-y-auto p-4 space-y-4" role="log" aria-label={t('chat.messages')}>
{currentMessages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<p className="text-text-tertiary text-lg mb-2">
{t('chat.startConversation')}
</p>
<p className="text-text-tertiary text-sm">
{t('chat.startConversationDesc')}
</p>
</div>
) : (
currentMessages.map((message) => (
<MessageBubble
key={message.id}
sender={message.sender === MessageSender.USER ? 'user' : message.sender === MessageSender.ASSISTANT ? 'ai' : 'system'}
content={message.content}
timestamp={new Date(message.createdAt)}
showActions
onCopy={() => {
navigator.clipboard.writeText(message.content);
setAnnouncement(t('chat.messageCopied'));
}}
/>
))
)}
{/* EN: Typing indicator / VI: Typing indicator */}
{(() => {
const { typingUsers } = useChatStore.getState();
return Object.values(typingUsers).some((typing) => typing) ? (
<React.Suspense fallback={<div className="px-4 py-3" aria-label={t('chat.loadingTypingIndicator')} />}>
<TypingIndicator />
</React.Suspense>
) : null;
})()}
</div>
{/* EN: Chat input / VI: Chat input */}
<ChatInput
onSend={handleSend}
placeholder={t('chat.typeMessage')}
/>
</ChatLayout>
</>
);
}

View File

@@ -1,586 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@/features/shared/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/features/shared/components/ui/dialog';
import {
Key,
Plus,
Trash2,
Copy,
Check,
Eye,
EyeOff,
AlertCircle,
CheckCircle2,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useI18n } from '@/features/theme';
import { useAuthStore } from '@/stores/auth-store';
import { apiKeysApi, type ApiKey } from '@/services/api/api-keys.api';
/**
* EN: Create API key schema with translated messages
* VI: Tạo API key schema với thông báo đã dịch
*/
function createApiKeySchema(
t: (key: string, values?: Record<string, any>) => string
) {
return z.object({
name: z
.string()
.min(1, t('settings.apiKeys.nameRequired'))
.max(100, t('validation.maxLength', { max: 100 })),
description: z
.string()
.max(500, t('validation.maxLength', { max: 500 }))
.optional(),
});
}
/**
* EN: Format timestamp to relative time string
* VI: Format timestamp thành chuỗi thời gian tương đối
*/
function formatRelativeTime(date: string | null, t: (key: string, values?: any) => string, locale: string): string {
if (!date) return t('settings.security.never');
const dateObj = new Date(date);
const now = new Date();
const diffMs = now.getTime() - dateObj.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return t('chat.justNow');
if (diffMins < 60) return t('chat.minutesAgo', { minutes: diffMins });
if (diffHours < 24) return t('chat.hoursAgo', { hours: diffHours });
if (diffDays < 7) return t('chat.daysAgo', { days: diffDays });
return dateObj.toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', {
month: 'short',
day: 'numeric',
year: dateObj.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
});
}
/**
* EN: Format date to readable string
* VI: Format ngày thành chuỗi dễ đọc
*/
function formatDate(date: string | null, t: (key: string) => string, locale: string): string {
if (!date) return t('settings.security.never');
return new Date(date).toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
/**
* EN: API Keys management page component
* VI: Component trang quản lý API Keys
*
* Features:
* - List all API keys
* - Create new API keys with name and description
* - Delete API keys
* - Show/hide API key values
* - Copy API key to clipboard
* - Display creation date, last used date, expiration date
*/
export default function ApiKeysPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const { locale } = useI18n();
const { user } = useAuthStore();
// EN: Create schema with translations / VI: Tạo schema với translations
const apiKeySchema = createApiKeySchema(t);
type CreateApiKeyFormData = z.infer<typeof apiKeySchema>;
// EN: State management / VI: Quản lý state
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false);
const [newApiKey, setNewApiKey] = useState<string>('');
const [newApiKeyName, setNewApiKeyName] = useState<string>('');
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
const [deletingKeyId, setDeletingKeyId] = useState<string | null>(null);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
// EN: React Hook Form setup for creating API key / VI: Setup React Hook Form cho tạo API key
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors, isSubmitting },
reset,
} = useForm<CreateApiKeyFormData>({
resolver: zodResolver(apiKeySchema),
defaultValues: {
name: '',
description: '',
},
});
// EN: Load API keys on mount / VI: Tải API keys khi mount
useEffect(() => {
if (user?.id) {
loadApiKeys();
}
}, [user?.id]);
/**
* EN: Load API keys from API
* VI: Tải API keys từ API
*/
const loadApiKeys = async () => {
if (!user?.id) return;
setIsLoading(true);
setError('');
try {
const response = await apiKeysApi.list(user.id);
if (response.success && response.data) {
setApiKeys(response.data);
}
} catch (err: any) {
setError(err.message || t('settings.apiKeys.failedToLoad'));
} finally {
setIsLoading(false);
}
};
/**
* EN: Handle create API key form submission
* VI: Xử lý submit form tạo API key
*/
const onCreateSubmit = async (data: CreateApiKeyFormData) => {
if (!user?.id) return;
setError('');
setSuccess('');
try {
const response = await apiKeysApi.create(user.id, {
name: data.name,
description: data.description,
});
if (!response.success || !response.data) {
throw new Error(t('settings.apiKeys.failedToCreate'));
}
setNewApiKey(response.data.key);
setNewApiKeyName(data.name);
setShowCreateDialog(false);
setShowNewKeyDialog(true);
reset();
setApiKeys((prev) => [response.data!.apiKey, ...prev]);
} catch (err: any) {
setError(err.message || t('settings.apiKeys.failedToCreate'));
}
};
/**
* EN: Handle delete API key
* VI: Xử lý xóa API key
*/
const handleDeleteKey = async (keyId: string, keyName: string) => {
if (!user?.id) return;
if (!confirm(t('settings.apiKeys.confirmDelete', { name: keyName }))) {
return;
}
setDeletingKeyId(keyId);
setError('');
try {
await apiKeysApi.delete(user.id, keyId);
setApiKeys((prev) => prev.filter((key) => key.id !== keyId));
setSuccess(t('settings.apiKeys.deletedSuccessfully'));
setTimeout(() => setSuccess(''), 3000);
} catch (err: any) {
setError(err.message || t('settings.apiKeys.failedToDelete'));
} finally {
setDeletingKeyId(null);
}
};
/**
* EN: Handle copy API key to clipboard
* VI: Xử lý sao chép API key vào clipboard
*/
const handleCopyKey = async (keyId: string, fullKey?: string) => {
try {
// EN: If full key is provided (from new key dialog), copy it / VI: Nếu có full key (từ dialog key mới), copy nó
if (fullKey) {
await navigator.clipboard.writeText(fullKey);
} else {
// EN: Existing keys are stored masked for security; copy masked value.
// VI: Các key đã lưu được che để bảo mật; sao chép giá trị đã che.
const key = apiKeys.find((k) => k.id === keyId);
if (key) {
await navigator.clipboard.writeText(`${key.keyPrefix}...${key.keySuffix}`);
}
}
setCopiedKeyId(keyId);
setTimeout(() => setCopiedKeyId(null), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
/**
* EN: Toggle visibility of API key
* VI: Bật/tắt hiển thị API key
*/
const toggleKeyVisibility = (keyId: string) => {
setVisibleKeys((prev) => {
const newSet = new Set(prev);
if (newSet.has(keyId)) {
newSet.delete(keyId);
} else {
newSet.add(keyId);
}
return newSet;
});
};
return (
<div className="space-y-6">
{/* EN: Page header / VI: Header trang */}
<div>
<h2 className="text-2xl font-semibold text-text-primary">
{t('settings.apiKeys.title')}
</h2>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.apiKeys.manageKeys')}
</p>
</div>
{/* EN: Success message / VI: Thông báo thành công */}
{success && (
<div className="rounded-lg bg-accent-success/10 border border-accent-success p-3 flex items-center gap-2 text-sm text-accent-success">
<CheckCircle2 className="h-4 w-4 flex-shrink-0" />
<span>{success}</span>
</div>
)}
{/* EN: Error message / VI: Thông báo lỗi */}
{error && (
<div className="rounded-lg bg-accent-error/10 border border-accent-error p-3 flex items-center gap-2 text-sm text-accent-error">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* EN: API Keys list card / VI: Card danh sách API Keys */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-accent-primary" />
{t('settings.apiKeys.yourApiKeys')}
</CardTitle>
<CardDescription className="mt-1">
{t('settings.apiKeys.createAndManage')}
</CardDescription>
</div>
<Button
variant="primary"
onClick={() => setShowCreateDialog(true)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
{t('settings.apiKeys.createApiKey')}
</Button>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-12 text-text-tertiary">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-accent-primary border-r-transparent mb-4"></div>
<p>{t('common.loading')}</p>
</div>
) : apiKeys.length === 0 ? (
<div className="text-center py-12">
<Key className="h-12 w-12 text-text-tertiary mx-auto mb-4" />
<p className="text-text-secondary font-medium mb-2">
{t('settings.apiKeys.noApiKeys')}
</p>
<p className="text-sm text-text-tertiary mb-6">
{t('settings.apiKeys.createFirstKey')}
</p>
<Button
variant="primary"
onClick={() => setShowCreateDialog(true)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
{t('settings.apiKeys.createApiKey')}
</Button>
</div>
) : (
<div className="space-y-3">
{apiKeys.map((apiKey) => (
<div
key={apiKey.id}
className="flex items-center justify-between p-4 rounded-lg bg-bg-tertiary border border-border-primary hover:border-border-secondary transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-sm font-semibold text-text-primary">
{apiKey.name}
</h3>
</div>
<div className="flex items-center gap-2 mb-2">
{/* EN: API Key display / VI: Hiển thị API Key */}
<code className="text-sm font-mono bg-bg-primary px-2 py-1 rounded border border-border-primary text-text-secondary">
{visibleKeys.has(apiKey.id)
? `${apiKey.keyPrefix}...${apiKey.keySuffix}`
: `${apiKey.keyPrefix}${'•'.repeat(20)}${apiKey.keySuffix}`}
</code>
<Button
variant="ghost"
size="sm"
onClick={() => toggleKeyVisibility(apiKey.id)}
className="h-7 w-7 p-0"
aria-label={visibleKeys.has(apiKey.id) ? t('settings.apiKeys.hide') : t('settings.apiKeys.show')}
>
{visibleKeys.has(apiKey.id) ? (
<EyeOff className="h-4 w-4 text-text-tertiary" />
) : (
<Eye className="h-4 w-4 text-text-tertiary" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyKey(apiKey.id)}
className="h-7 w-7 p-0"
aria-label={t('settings.apiKeys.copy')}
>
{copiedKeyId === apiKey.id ? (
<Check className="h-4 w-4 text-accent-success" />
) : (
<Copy className="h-4 w-4 text-text-tertiary" />
)}
</Button>
</div>
<div className="flex items-center gap-4 text-xs text-text-tertiary">
<span>
{t('settings.apiKeys.created')}: {formatDate(apiKey.createdAt, t, locale)}
</span>
{apiKey.lastUsedAt && (
<span>
{t('settings.apiKeys.lastUsed')}: {formatRelativeTime(apiKey.lastUsedAt, t, locale)}
</span>
)}
{apiKey.expiresAt && (
<span className={new Date(apiKey.expiresAt) < new Date() ? 'text-accent-error' : ''}>
{t('settings.apiKeys.expires')}: {formatDate(apiKey.expiresAt, t, locale)}
</span>
)}
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteKey(apiKey.id, apiKey.name)}
isLoading={deletingKeyId === apiKey.id}
className="text-accent-error hover:brightness-110 hover:bg-accent-error/10 ml-4"
aria-label={`${t('settings.apiKeys.delete')} ${apiKey.name}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* EN: Security notice card / VI: Card thông báo bảo mật */}
<Card hover={false} bordered>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<AlertCircle className="h-5 w-5 text-accent-warning" />
{t('settings.apiKeys.securityBestPractices', { defaultValue: 'Security Best Practices' })}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-text-secondary">
<ul className="list-disc list-inside space-y-1 ml-2">
<li>{t('settings.apiKeys.practice1', { defaultValue: 'Keep your API keys secure and never share them publicly' })}</li>
<li>{t('settings.apiKeys.practice2', { defaultValue: 'Use environment variables or secure secret management tools' })}</li>
<li>{t('settings.apiKeys.practice3', { defaultValue: 'Rotate your API keys regularly' })}</li>
<li>{t('settings.apiKeys.practice4', { defaultValue: 'Delete unused API keys immediately' })}</li>
<li>{t('settings.apiKeys.practice5', { defaultValue: 'If a key is compromised, revoke it immediately and create a new one' })}</li>
</ul>
</CardContent>
</Card>
{/* EN: Create API Key Dialog / VI: Dialog tạo API Key */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('settings.apiKeys.createApiKey')}</DialogTitle>
<DialogDescription>
{t('settings.apiKeys.createForAccess', { defaultValue: 'Create a new API key for programmatic access' })}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(onCreateSubmit)} className="space-y-4">
{/* EN: Error message / VI: Thông báo lỗi */}
{error && (
<div className="p-3 rounded-md bg-bg-tertiary border border-accent-error text-accent-error text-sm flex items-center gap-2">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<Input
label={t('settings.apiKeys.name')}
placeholder={t('settings.apiKeys.namePlaceholder', { defaultValue: 'e.g., Production Key, Development Key' })}
name="name"
value={watch('name')}
onChange={(value) => setValue('name', value)}
onBlur={register('name').onBlur}
errorMessage={errors.name?.message}
isInvalid={!!errors.name}
autoFocus
/>
<div>
<label
htmlFor="description"
className="block text-sm font-medium text-text-secondary mb-2"
>
{t('settings.apiKeys.description')} ({t('common.optional', { defaultValue: 'Optional' })})
</label>
<textarea
id="description"
rows={3}
placeholder={t('settings.apiKeys.descriptionPlaceholder', { defaultValue: 'Optional description for this API key' })}
{...register('description')}
className="flex w-full rounded-md border border-border-primary bg-bg-secondary px-3 py-2 text-base text-text-primary placeholder:text-text-tertiary transition-all duration-[150ms] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:border-accent-primary focus-visible:ring-accent-primary focus-visible:shadow-[0_0_20px_rgba(59,130,246,0.3)] disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-tertiary"
/>
{errors.description && (
<p className="mt-1.5 text-sm text-accent-error flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{errors.description.message}
</p>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => {
setShowCreateDialog(false);
reset();
setError('');
}}
>
{t('common.cancel')}
</Button>
<Button type="submit" isLoading={isSubmitting}>
{t('settings.apiKeys.createApiKey')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* EN: New API Key Dialog (shown once after creation) / VI: Dialog API Key mới (hiển thị một lần sau khi tạo) */}
<Dialog open={showNewKeyDialog} onOpenChange={setShowNewKeyDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-accent-success" />
{t('settings.apiKeys.newApiKeyCreated')}
</DialogTitle>
<DialogDescription>
{t('settings.apiKeys.newKeyFor', { name: newApiKeyName, defaultValue: `Your new API key for "${newApiKeyName}"` })}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* EN: Warning message / VI: Thông báo cảnh báo */}
<div className="p-3 rounded-md bg-accent-warning/10 border border-accent-warning text-accent-warning text-sm">
<p className="font-medium mb-1">
{t('settings.apiKeys.important', { defaultValue: 'Important' })}
</p>
<p>
{t('settings.apiKeys.saveKeySecurely')}
</p>
</div>
{/* EN: API Key display / VI: Hiển thị API Key */}
<div className="p-4 rounded-lg bg-bg-primary border border-border-primary">
<code className="text-sm font-mono text-text-primary break-all select-all">
{newApiKey}
</code>
</div>
{/* EN: Copy button / VI: Nút sao chép */}
<Button
variant="primary"
onClick={() => handleCopyKey('new', newApiKey)}
className="w-full flex items-center justify-center gap-2"
>
{copiedKeyId === 'new' ? (
<>
<Check className="h-4 w-4" />
Copied! / Đã sao chép!
</>
) : (
<>
<Copy className="h-4 w-4" />
{t('settings.apiKeys.copyApiKey', { defaultValue: 'Copy API Key' })}
</>
)}
</Button>
</div>
<DialogFooter>
<Button
variant="primary"
onClick={() => {
setShowNewKeyDialog(false);
setNewApiKey('');
setNewApiKeyName('');
}}
className="w-full"
>
{t('settings.apiKeys.iveCopied', { defaultValue: 'I\'ve copied the key' })}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,146 +0,0 @@
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { cn } from '@/shared/lib/utils';
import {
User,
Settings,
Shield,
Bell,
CreditCard,
Key,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
/**
* EN: Settings navigation tabs configuration
* VI: Cấu hình các tab điều hướng Settings
*/
function getSettingsTabs(t: (key: string) => string) {
return [
{
id: 'profile',
label: t('settings.profile.label'),
href: '/settings/profile',
icon: User,
},
{
id: 'preferences',
label: t('settings.preferences.label'),
href: '/settings/preferences',
icon: Settings,
},
{
id: 'security',
label: t('settings.security.label'),
href: '/settings/security',
icon: Shield,
},
{
id: 'notifications',
label: t('settings.notifications.label'),
href: '/settings/notifications',
icon: Bell,
},
{
id: 'billing',
label: t('settings.billing.label'),
href: '/settings/billing',
icon: CreditCard,
},
{
id: 'api-keys',
label: t('settings.apiKeys.label'),
href: '/settings/api-keys',
icon: Key,
},
];
}
/**
* EN: Settings layout component with tab navigation
* VI: Component layout Settings với điều hướng tab
*
* Features:
* - Tab navigation for different settings sections
* - Active tab highlighting
* - Responsive design
* - Icon support for each tab
*/
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const pathname = usePathname();
const settingsTabs = getSettingsTabs(t);
return (
<div className="min-h-screen bg-bg-primary">
{/* EN: Settings header / VI: Header Settings */}
<div className="border-b border-border-primary bg-bg-secondary">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="py-6">
<h1 className="text-3xl font-bold text-text-primary">
{t('settings.title')}
</h1>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.description')}
</p>
</div>
</div>
</div>
{/* EN: Settings content with tabs / VI: Nội dung Settings với tabs */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="py-8">
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
{/* EN: Sidebar navigation / VI: Điều hướng sidebar */}
<aside className="lg:col-span-3">
<nav
className="space-y-1"
aria-label={t('settings.navigation')}
>
{settingsTabs.map((tab) => {
const Icon = tab.icon;
const isActive = pathname === tab.href;
return (
<Link
key={tab.id}
href={tab.href}
className={cn(
// EN: Base styles / VI: Styles cơ bản
'group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-[150ms]',
// EN: Active state / VI: Trạng thái active
isActive
? 'bg-accent-primary text-white shadow-md'
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary',
)}
aria-current={isActive ? 'page' : undefined}
>
<Icon
className={cn(
'h-5 w-5 flex-shrink-0',
isActive ? 'text-white' : 'text-text-tertiary group-hover:text-text-primary',
)}
aria-hidden="true"
/>
<span>{tab.label}</span>
</Link>
);
})}
</nav>
</aside>
{/* EN: Main content area / VI: Khu vực nội dung chính */}
<div className="mt-8 lg:col-span-9 lg:mt-0">{children}</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,362 +0,0 @@
'use client';
import * as React from 'react';
import { useTheme, type ThemeMode } from '@/features/theme';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/features/shared/components/ui/card';
import { Select } from '@/features/shared/components/ui/select';
import { Switch } from '@/features/shared/components/ui/switch';
import { Button } from '@/features/shared/components/ui/button';
import { useTranslations } from 'next-intl';
import { useI18n } from '@/features/theme';
import { type Locale } from '@/features/theme/i18n-config';
/**
* EN: Language options for preferences
* VI: Các tùy chọn ngôn ngữ cho preferences
*/
type Language = 'en' | 'vi';
/**
* EN: Font size options for chat
* VI: Các tùy chọn kích thước font cho chat
*/
type FontSize = 'small' | 'medium' | 'large' | 'xlarge';
/**
* EN: Message grouping options
* VI: Các tùy chọn nhóm tin nhắn
*/
type MessageGrouping = 'none' | 'by-author' | 'by-time';
/**
* EN: Preferences interface
* VI: Interface cho preferences
*/
interface Preferences {
language: Language;
theme: ThemeMode;
chatAutoScroll: boolean;
chatShowTimestamps: boolean;
chatMessageGrouping: MessageGrouping;
chatFontSize: FontSize;
accessibilityHighContrast: boolean;
accessibilityScreenReader: boolean;
}
/**
* EN: Default preferences
* VI: Preferences mặc định
*/
const defaultPreferences: Preferences = {
language: 'en',
theme: 'system',
chatAutoScroll: true,
chatShowTimestamps: true,
chatMessageGrouping: 'by-author',
chatFontSize: 'medium',
accessibilityHighContrast: false,
accessibilityScreenReader: false,
};
/**
* EN: Preferences page component
* VI: Component trang Preferences
*
* Features:
* - Language selection
* - Theme selection (Dark/Light/Auto)
* - Chat settings (Auto-scroll, Show timestamps, Message grouping, Font size)
* - Accessibility options (High contrast mode, Screen reader optimizations)
*/
export default function PreferencesPage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const { locale, setLocale } = useI18n();
const { theme, setTheme } = useTheme();
const [preferences, setPreferences] = React.useState<Preferences>(defaultPreferences);
const [isSaving, setIsSaving] = React.useState(false);
const [saveSuccess, setSaveSuccess] = React.useState(false);
// EN: Load preferences from localStorage on mount / VI: Load preferences từ localStorage khi mount
React.useEffect(() => {
if (typeof window === 'undefined') return;
const stored = localStorage.getItem('preferences');
if (stored) {
try {
const parsed = JSON.parse(stored) as Partial<Preferences>;
const loaded = { ...defaultPreferences, ...parsed, theme, language: locale as Language };
setPreferences(loaded);
} catch {
// EN: Invalid stored data, use defaults / VI: Dữ liệu lưu không hợp lệ, dùng mặc định
setPreferences({ ...defaultPreferences, theme, language: locale as Language });
}
} else {
setPreferences({ ...defaultPreferences, theme, language: locale as Language });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // EN: Only run on mount / VI: Chỉ chạy khi mount
// EN: Sync locale with preferences / VI: Đồng bộ locale với preferences
React.useEffect(() => {
if (preferences.language !== locale) {
setPreferences((prev) => ({ ...prev, language: locale as Language }));
}
}, [locale, preferences.language]);
// EN: Update preferences state / VI: Cập nhật state preferences
const updatePreference = <K extends keyof Preferences>(
key: K,
value: Preferences[K]
) => {
setPreferences((prev) => ({ ...prev, [key]: value }));
};
// EN: Handle save preferences / VI: Xử lý lưu preferences
const handleSave = async () => {
setIsSaving(true);
setSaveSuccess(false);
try {
// EN: Save to localStorage / VI: Lưu vào localStorage
localStorage.setItem('preferences', JSON.stringify(preferences));
// EN: Show success message / VI: Hiển thị thông báo thành công
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (error) {
console.error('Failed to save preferences:', error);
} finally {
setIsSaving(false);
}
};
// EN: Handle theme change / VI: Xử lý thay đổi theme
const handleThemeChange = (newTheme: ThemeMode) => {
setTheme(newTheme);
updatePreference('theme', newTheme);
};
// EN: Handle language change / VI: Xử lý thay đổi ngôn ngữ
const handleLanguageChange = (newLanguage: string) => {
const lang = newLanguage as Locale;
setLocale(lang);
updatePreference('language', lang as Language);
// EN: Trigger a re-render by updating preferences / VI: Kích hoạt re-render bằng cách cập nhật preferences
setPreferences((prev) => ({ ...prev, language: lang as Language }));
};
return (
<div className="space-y-6">
{/* EN: Page header / VI: Header trang */}
<div>
<h2 className="text-2xl font-semibold text-text-primary">
{t('settings.preferences.title')}
</h2>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.preferences.description')}
</p>
</div>
{/* EN: Language & Theme Section / VI: Phần Ngôn ngữ & Theme */}
<Card>
<CardHeader>
<CardTitle>{t('settings.preferences.languageAndTheme')}</CardTitle>
<CardDescription>
{t('settings.preferences.languageAndThemeDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* EN: Language selection / VI: Chọn ngôn ngữ */}
<div>
<Select
label={t('settings.preferences.language')}
value={preferences.language}
onChange={(e) => handleLanguageChange(e.target.value)}
helperText={t('settings.preferences.languageHelper')}
>
<option value="en">English</option>
<option value="vi">Tiếng Việt</option>
</Select>
</div>
{/* EN: Theme selection / VI: Chọn theme */}
<div>
<Select
label={t('settings.preferences.theme')}
value={preferences.theme}
onChange={(e) => handleThemeChange(e.target.value as ThemeMode)}
helperText={t('settings.preferences.themeHelper')}
>
<option value="light">{t('settings.preferences.light')}</option>
<option value="dark">{t('settings.preferences.dark')}</option>
<option value="system">{t('settings.preferences.system')}</option>
</Select>
</div>
</CardContent>
</Card>
{/* EN: Chat Settings Section / VI: Phần Cài đặt Chat */}
<Card>
<CardHeader>
<CardTitle>{t('settings.preferences.chatSettings')}</CardTitle>
<CardDescription>
{t('settings.preferences.chatSettingsDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* EN: Auto-scroll toggle / VI: Toggle tự động cuộn */}
<div className="flex items-center justify-between">
<div className="flex-1">
<label
htmlFor="chat-auto-scroll"
className="text-sm font-medium text-text-primary cursor-pointer"
>
{t('settings.preferences.autoScroll')}
</label>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.preferences.autoScrollDesc')}
</p>
</div>
<Switch
id="chat-auto-scroll"
checked={preferences.chatAutoScroll}
onCheckedChange={(checked) => updatePreference('chatAutoScroll', checked)}
/>
</div>
{/* EN: Show timestamps toggle / VI: Toggle hiển thị thời gian */}
<div className="flex items-center justify-between">
<div className="flex-1">
<label
htmlFor="chat-show-timestamps"
className="text-sm font-medium text-text-primary cursor-pointer"
>
{t('settings.preferences.showTimestamps')}
</label>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.preferences.showTimestampsDesc')}
</p>
</div>
<Switch
id="chat-show-timestamps"
checked={preferences.chatShowTimestamps}
onCheckedChange={(checked) => updatePreference('chatShowTimestamps', checked)}
/>
</div>
{/* EN: Message grouping / VI: Nhóm tin nhắn */}
<div>
<Select
label={t('settings.preferences.messageGrouping')}
value={preferences.chatMessageGrouping}
onChange={(e) => updatePreference('chatMessageGrouping', e.target.value as MessageGrouping)}
helperText={t('settings.preferences.messageGroupingHelper')}
>
<option value="none">{t('settings.preferences.none')}</option>
<option value="by-author">{t('settings.preferences.byAuthor')}</option>
<option value="by-time">{t('settings.preferences.byTime')}</option>
</Select>
</div>
{/* EN: Font size / VI: Kích thước font */}
<div>
<Select
label={t('settings.preferences.fontSize')}
value={preferences.chatFontSize}
onChange={(e) => updatePreference('chatFontSize', e.target.value as FontSize)}
helperText={t('settings.preferences.fontSizeHelper')}
>
<option value="small">{t('settings.preferences.small')}</option>
<option value="medium">{t('settings.preferences.medium')}</option>
<option value="large">{t('settings.preferences.large')}</option>
<option value="xlarge">{t('settings.preferences.extraLarge')}</option>
</Select>
</div>
</CardContent>
</Card>
{/* EN: Accessibility Section / VI: Phần Khả năng truy cập */}
<Card>
<CardHeader>
<CardTitle>{t('settings.preferences.accessibility')}</CardTitle>
<CardDescription>
{t('settings.preferences.accessibilityDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* EN: High contrast mode toggle / VI: Toggle chế độ tương phản cao */}
<div className="flex items-center justify-between">
<div className="flex-1">
<label
htmlFor="accessibility-high-contrast"
className="text-sm font-medium text-text-primary cursor-pointer"
>
{t('settings.preferences.highContrast')}
</label>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.preferences.highContrastDesc')}
</p>
</div>
<Switch
id="accessibility-high-contrast"
checked={preferences.accessibilityHighContrast}
onCheckedChange={(checked) => updatePreference('accessibilityHighContrast', checked)}
/>
</div>
{/* EN: Screen reader optimizations toggle / VI: Toggle tối ưu screen reader */}
<div className="flex items-center justify-between">
<div className="flex-1">
<label
htmlFor="accessibility-screen-reader"
className="text-sm font-medium text-[#FAFAFA] cursor-pointer"
>
{t('settings.preferences.screenReader')}
</label>
<p className="mt-1 text-sm text-[#A0A0A0]">
{t('settings.preferences.screenReaderDesc')}
</p>
</div>
<Switch
id="accessibility-screen-reader"
checked={preferences.accessibilityScreenReader}
onCheckedChange={(checked) => updatePreference('accessibilityScreenReader', checked)}
/>
</div>
</CardContent>
</Card>
{/* EN: Save button / VI: Nút lưu */}
<div className="flex items-center justify-end gap-4">
{saveSuccess && (
<p className="text-sm text-accent-success flex items-center gap-2">
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
{t('settings.preferences.preferencesSaved')}
</p>
)}
<Button
variant="primary"
onClick={handleSave}
isLoading={isSaving}
isDisabled={isSaving}
>
{t('settings.preferences.savePreferences')}
</Button>
</div>
</div>
);
}

View File

@@ -1,450 +0,0 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
AlertCircle,
CheckCircle2,
Upload,
UserCircle2,
} from 'lucide-react';
import { useAuthStore } from '@/stores/auth-store';
import { userApi, type UserProfile, type UpdateUserProfileDto } from '@/services/api/user.api';
import { storageApi } from '@/services/api/storage.api';
import { useTranslations } from 'next-intl';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/features/shared/components/ui/card';
/**
* EN: Create profile schema with translated messages
* VI: Tạo profile schema với thông báo đã dịch
*/
function createProfileSchema(
t: (key: string, values?: Record<string, any>) => string
) {
return z.object({
firstName: z
.string()
.max(255, t('validation.maxLength', { max: 255 }))
.optional(),
lastName: z
.string()
.max(255, t('validation.maxLength', { max: 255 }))
.optional(),
phone: z
.string()
.max(20, t('validation.maxLength', { max: 20 }))
.optional(),
bio: z
.string()
.max(500, t('validation.maxLength', { max: 500 }))
.optional(),
});
}
/**
* EN: User Profile page component
* VI: Component trang Profile người dùng
*
* Features:
* - Avatar upload with preview
* - Profile fields (First Name, Last Name, Phone, Bio)
* - Email display with verified status
* - Username display
* - Save changes button
* - Loading states
* - Error handling
*/
export default function ProfilePage() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const { user } = useAuthStore();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
// EN: Create schema with translations / VI: Tạo schema với translations
const profileSchema = createProfileSchema(t);
type ProfileFormData = z.infer<typeof profileSchema>;
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
setValue,
watch,
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: {
firstName: '',
lastName: '',
phone: '',
bio: '',
},
});
// EN: Fetch user profile on mount / VI: Lấy profile người dùng khi mount
useEffect(() => {
const fetchProfile = async () => {
if (!user?.id) return;
try {
setIsLoading(true);
setErrorMessage('');
const response = await userApi.getProfile(user.id);
if (response.success && response.data) {
setProfile(response.data);
const attributes = response.data.attributes ?? [];
const firstName = attributes.find((attr) => attr.key === 'firstName')?.value ?? '';
const lastName = attributes.find((attr) => attr.key === 'lastName')?.value ?? '';
const phone = attributes.find((attr) => attr.key === 'phone')?.value ?? '';
setValue('firstName', firstName, { shouldDirty: false });
setValue('lastName', lastName, { shouldDirty: false });
setValue('phone', phone, { shouldDirty: false });
setValue('bio', response.data.bio ?? '', { shouldDirty: false });
if (response.data.avatarUrl) {
setAvatarPreview(response.data.avatarUrl);
}
}
} catch (error) {
console.error('Failed to fetch profile:', error);
setErrorMessage(t('settings.profile.failedToFetch'));
} finally {
setIsLoading(false);
}
};
fetchProfile();
}, [user?.id, setValue]);
// EN: Handle avatar file selection / VI: Xử lý chọn file avatar
const handleAvatarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// EN: Validate file type / VI: Kiểm tra loại file
if (!file.type.startsWith('image/')) {
alert(t('settings.profile.selectImageFile'));
return;
}
// EN: Validate file size (max 5MB) / VI: Kiểm tra kích thước file (tối đa 5MB)
if (file.size > 5 * 1024 * 1024) {
alert(t('settings.profile.imageSizeLimit'));
return;
}
setAvatarFile(file);
// EN: Create preview URL / VI: Tạo URL preview
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
};
// EN: Handle avatar upload / VI: Xử lý upload avatar
const handleAvatarUpload = async () => {
if (!user?.id || !avatarFile) return;
try {
setIsUploadingAvatar(true);
setSaveStatus('idle');
setErrorMessage('');
const uploadResponse = await storageApi.uploadAvatar(avatarFile);
const avatarUrl =
uploadResponse.data?.url ||
`/files/${uploadResponse.data?.fileId}/cdn-url`;
const response = await userApi.uploadAvatar(user.id, avatarUrl);
if (!response.success || !response.data) {
throw new Error(t('settings.profile.failedToUpload'));
}
setProfile(response.data);
setAvatarPreview(response.data.avatarUrl ?? avatarPreview);
setAvatarFile(null);
setSaveStatus('success');
setTimeout(() => setSaveStatus('idle'), 3000);
} catch (error) {
console.error('Failed to upload avatar:', error);
setSaveStatus('error');
setErrorMessage(t('settings.profile.failedToUpload'));
} finally {
setIsUploadingAvatar(false);
}
};
// EN: Handle form submission / VI: Xử lý submit form
const onSubmit = async (data: ProfileFormData) => {
if (!user?.id) return;
try {
setIsSaving(true);
setSaveStatus('idle');
setErrorMessage('');
const updateData: UpdateUserProfileDto = {
bio: data.bio || undefined,
};
const response = await userApi.updateProfile(user.id, updateData);
if (!response.success || !response.data) {
throw new Error(t('settings.profile.failedToUpdate'));
}
await Promise.all([
userApi.setProfileAttribute(user.id, 'firstName', {
value: data.firstName ?? '',
valueType: 'String',
}),
userApi.setProfileAttribute(user.id, 'lastName', {
value: data.lastName ?? '',
valueType: 'String',
}),
userApi.setProfileAttribute(user.id, 'phone', {
value: data.phone ?? '',
valueType: 'String',
}),
]);
setProfile(response.data);
reset(data);
setSaveStatus('success');
setTimeout(() => setSaveStatus('idle'), 3000);
} catch (error) {
console.error('Failed to update profile:', error);
setSaveStatus('error');
setErrorMessage(t('settings.profile.failedToUpdate'));
} finally {
setIsSaving(false);
}
};
// EN: Get user initials for avatar fallback / VI: Lấy chữ cái đầu cho avatar fallback
const getUserInitials = () => {
if (!user) return 'U';
const attributes = profile?.attributes ?? [];
const firstName = attributes.find((attr) => attr.key === 'firstName')?.value || user.email.split('@')[0];
const lastName = attributes.find((attr) => attr.key === 'lastName')?.value || '';
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase() || 'U';
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-accent-primary border-r-transparent"></div>
<p className="mt-4 text-sm text-text-tertiary">
{t('common.loading')}
</p>
</div>
</div>
);
}
const username = user?.email?.split('@')[0] ?? t('settings.profile.notSet');
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold text-text-primary">
{t('settings.profile.title')}
</h2>
<p className="mt-1 text-sm text-text-tertiary">
{t('settings.profile.updateInfo')}
</p>
</div>
{saveStatus === 'success' && (
<div className="rounded-lg bg-accent-success/10 border border-accent-success p-3 flex items-center gap-2 text-sm text-accent-success">
<CheckCircle2 className="h-4 w-4 flex-shrink-0" />
<span>{t('settings.profile.changesSaved')}</span>
</div>
)}
{(saveStatus === 'error' || errorMessage) && (
<div className="rounded-lg bg-accent-error/10 border border-accent-error p-3 flex items-center gap-2 text-sm text-accent-error">
<AlertCircle className="h-4 w-4 flex-shrink-0" />
<span>{errorMessage || t('settings.profile.failedToUpdate')}</span>
</div>
)}
<Card bordered>
<CardHeader>
<CardTitle>{t('settings.profile.changeAvatar')}</CardTitle>
<CardDescription>{t('settings.profile.updateInfo')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="h-24 w-24 rounded-full bg-bg-primary border border-border-primary flex items-center justify-center overflow-hidden">
{avatarPreview ? (
<img
src={avatarPreview}
alt={t('settings.profile.changeAvatar')}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full flex items-center justify-center text-lg font-semibold text-text-secondary">
{getUserInitials()}
</div>
)}
</div>
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
<Button
variant="secondary"
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2"
>
<UserCircle2 className="h-4 w-4" />
{t('settings.profile.changeAvatar')}
</Button>
<Button
variant="primary"
onClick={handleAvatarUpload}
isLoading={isUploadingAvatar}
isDisabled={!avatarFile}
className="flex items-center gap-2"
>
<Upload className="h-4 w-4" />
{t('settings.profile.uploadAvatar')}
</Button>
</div>
{avatarFile && (
<p className="text-xs text-text-tertiary">{avatarFile.name}</p>
)}
</div>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleAvatarChange}
/>
</CardContent>
</Card>
<Card bordered>
<CardHeader>
<CardTitle>{t('settings.profile.title')}</CardTitle>
<CardDescription>{t('settings.profile.updateInfo')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t('settings.profile.firstName')}
placeholder={t('settings.profile.enterFirstName')}
value={watch('firstName') ?? ''}
onChange={(value) => setValue('firstName', value, { shouldDirty: true })}
onBlur={register('firstName').onBlur}
errorMessage={errors.firstName?.message}
isInvalid={!!errors.firstName}
/>
<Input
label={t('settings.profile.lastName')}
placeholder={t('settings.profile.enterLastName')}
value={watch('lastName') ?? ''}
onChange={(value) => setValue('lastName', value, { shouldDirty: true })}
onBlur={register('lastName').onBlur}
errorMessage={errors.lastName?.message}
isInvalid={!!errors.lastName}
/>
</div>
<Input
label={t('settings.profile.phone')}
placeholder={t('settings.profile.enterPhone')}
value={watch('phone') ?? ''}
onChange={(value) => setValue('phone', value, { shouldDirty: true })}
onBlur={register('phone').onBlur}
errorMessage={errors.phone?.message}
isInvalid={!!errors.phone}
/>
<div>
<label
htmlFor="bio"
className="block text-sm font-medium text-text-secondary mb-2"
>
{t('settings.profile.bio')}
</label>
<textarea
id="bio"
rows={4}
value={watch('bio') ?? ''}
onChange={(event) =>
setValue('bio', event.target.value, { shouldDirty: true })
}
onBlur={register('bio').onBlur}
placeholder={t('settings.profile.bioPlaceholder')}
className="flex w-full rounded-md border border-border-primary bg-bg-secondary px-3 py-2 text-base text-text-primary placeholder:text-text-tertiary transition-all duration-[150ms] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:border-accent-primary focus-visible:ring-accent-primary"
/>
{errors.bio && (
<p className="mt-1.5 text-sm text-accent-error">
{errors.bio.message}
</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t('settings.profile.email')}
value={user?.email ?? ''}
isDisabled
description={t('settings.profile.emailCannotChange')}
/>
<Input
label={t('settings.profile.username')}
value={username}
isDisabled
description={t('settings.profile.usernameCannotChange')}
/>
</div>
<div className="pt-2">
<Button
type="submit"
variant="primary"
isLoading={isSaving}
isDisabled={!isDirty}
>
{t('settings.profile.saveChanges')}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,272 +0,0 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, User, Mail, Calendar, Shield, Edit } from 'lucide-react';
import { Role } from '@goodgo/types';
import { useUsersStore } from '../../../../stores/users-store';
import { UserCard, UserForm } from '../../../../features/shared/components/users';
import { Button } from '../../../../features/shared/components/ui/button';
import { Card } from '../../../../features/shared/components/ui/card';
import { AuthGuard } from '../../../../features/shared/middleware/auth-guard';
import { useAuthStore } from '../../../../stores/auth-store';
/**
* EN: Admin User Detail/Edit Page
* VI: Trang chi tiết/edit User cho Admin
*
* Features:
* - Display user details
* - Edit user information
* - Role management
* - Activity logs (placeholder)
* - Breadcrumb navigation
*/
export default function AdminUserDetailPage() {
const params = useParams();
const router = useRouter();
const userId = params.id as string;
const { user: currentUser } = useAuthStore();
const {
currentUser: user,
isLoadingUser,
error,
fetchUser,
updateUser,
clearCurrentUser,
clearError,
} = useUsersStore();
const [isEditing, setIsEditing] = useState(false);
// Check permissions
const isAdmin = currentUser?.role === Role.ADMIN || currentUser?.role === Role.SUPER_ADMIN;
const canEdit = currentUser?.role === Role.SUPER_ADMIN ||
(currentUser?.role === Role.ADMIN && user?.role !== Role.SUPER_ADMIN);
// Load user data
useEffect(() => {
if (isAdmin && userId) {
fetchUser(userId);
}
}, [isAdmin, userId, fetchUser]);
// Cleanup on unmount
useEffect(() => {
return () => {
clearCurrentUser();
};
}, [clearCurrentUser]);
const handleEditSubmit = async (userData: any) => {
if (!user) return;
try {
await updateUser(user.id, userData);
setIsEditing(false);
clearError();
} catch (error) {
// Error handled in store
}
};
if (isLoadingUser) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8 text-center max-w-md">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error</h2>
<p className="text-gray-600 mb-6">{error}</p>
<div className="flex gap-4 justify-center">
<Button onClick={() => fetchUser(userId)}>
Try Again
</Button>
<Button variant="secondary" onClick={() => router.back()}>
Go Back
</Button>
</div>
</Card>
</div>
);
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">User Not Found</h2>
<p className="text-gray-600 mb-6">
The user you're looking for doesn't exist.
</p>
<Button onClick={() => router.push('/admin/users')}>
Back to Users
</Button>
</Card>
</div>
);
}
return (
<AuthGuard requiredRoles={[Role.ADMIN, Role.SUPER_ADMIN]}>
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-4">
<Button
variant="secondary"
size="sm"
onClick={() => router.back()}
className="flex items-center space-x-2"
>
<ArrowLeft className="w-4 h-4" />
<span>Back</span>
</Button>
<div>
<h1 className="text-2xl font-bold text-gray-900">User Details</h1>
<p className="text-gray-600">{user.email}</p>
</div>
</div>
{canEdit && !isEditing && (
<Button
onClick={() => setIsEditing(true)}
className="flex items-center space-x-2"
>
<Edit className="w-4 h-4" />
<span>Edit User</span>
</Button>
)}
</div>
{/* Error Display */}
{error && (
<Card className="p-4 mb-6 bg-red-50 border-red-200">
<div className="flex items-center justify-between">
<p className="text-red-800">{error}</p>
<Button variant="secondary" size="sm" onClick={clearError}>
Dismiss
</Button>
</div>
</Card>
)}
{/* Content */}
{isEditing ? (
<UserForm
user={user}
loading={isLoadingUser}
onSubmit={handleEditSubmit}
onCancel={() => setIsEditing(false)}
/>
) : (
<div className="space-y-6">
{/* User Card */}
<UserCard
user={user}
showAdminActions={false}
className="mb-6"
/>
{/* User Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Account Information */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-4">
<User className="w-6 h-6 text-blue-600" />
<h3 className="text-lg font-semibold text-gray-900">Account Information</h3>
</div>
<dl className="space-y-3">
<div>
<dt className="text-sm font-medium text-gray-500">User ID</dt>
<dd className="text-sm text-gray-900 font-mono">{user.id}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="text-sm text-gray-900">{user.email}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Role</dt>
<dd className="text-sm text-gray-900">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.role === 'SUPER_ADMIN' ? 'bg-red-100 text-red-800' :
user.role === 'ADMIN' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{user.role}
</span>
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Status</dt>
<dd className="text-sm text-gray-900">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{user.isActive ? 'Active' : 'Inactive'}
</span>
</dd>
</div>
</dl>
</Card>
{/* Account Timeline */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-4">
<Calendar className="w-6 h-6 text-green-600" />
<h3 className="text-lg font-semibold text-gray-900">Account Timeline</h3>
</div>
<dl className="space-y-3">
<div>
<dt className="text-sm font-medium text-gray-500">Created</dt>
<dd className="text-sm text-gray-900">
{new Date(user.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Last Updated</dt>
<dd className="text-sm text-gray-900">
{new Date(user.updatedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</dd>
</div>
</dl>
</Card>
</div>
{/* Activity Logs (Placeholder) */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-4">
<Shield className="w-6 h-6 text-purple-600" />
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div className="text-center py-8">
<p className="text-gray-500">
Activity logs will be displayed here in a future update.
</p>
</div>
</Card>
</div>
)}
</div>
</div>
</AuthGuard>
);
}

View File

@@ -1,317 +0,0 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Plus, Search, Filter, ArrowLeft } from 'lucide-react';
import { Role } from '@goodgo/types';
import { useUsersStore } from '../../../stores/users-store';
import { UsersTable, UserForm } from '../../../features/shared/components/users';
import { Button } from '../../../features/shared/components/ui/button';
import { Input } from '../../../features/shared/components/ui/input';
import { Card } from '../../../features/shared/components/ui/card';
import { ResponsiveLayout } from '../../../features/shared/components/layout/responsive-layout';
import { AuthGuard } from '../../../features/shared/middleware/auth-guard';
import { useAuthStore } from '../../../stores/auth-store';
import { useRouter } from 'next/navigation';
/**
* EN: Admin Users List Page
* VI: Trang danh sách Users cho Admin
*
* Features:
* - List all users with pagination
* - Search and filter functionality
* - Bulk actions (activate/deactivate/delete)
* - Create new user modal
* - Edit user modal
* - Role-based access control
*/
export default function AdminUsersPage() {
const { user: currentUser } = useAuthStore();
const router = useRouter();
const {
users,
pagination,
isLoading,
error,
fetchUsers,
createUser,
updateUser,
deleteUser,
bulkDeleteUsers,
bulkUpdateUserRoles,
clearError,
} = useUsersStore();
const [searchQuery, setSearchQuery] = useState('');
const [selectedRole, setSelectedRole] = useState<string>('all');
const [selectedStatus, setSelectedStatus] = useState<string>('all');
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingUser, setEditingUser] = useState<any>(null);
// Check if current user has admin permissions
const isAdmin = currentUser?.role === Role.ADMIN || currentUser?.role === Role.SUPER_ADMIN;
const isSuperAdmin = currentUser?.role === Role.SUPER_ADMIN;
// Load users on mount and when filters change
useEffect(() => {
if (isAdmin) {
const params = {
search: searchQuery || undefined,
role: selectedRole !== 'all' ? (selectedRole as Role) : undefined,
isActive: selectedStatus !== 'all' ? selectedStatus === 'active' : undefined,
limit: 20,
};
fetchUsers(params as any);
}
}, [isAdmin, searchQuery, selectedRole, selectedStatus, fetchUsers]);
const handleCreateUser = async (userData: any) => {
try {
await createUser(userData);
setShowCreateModal(false);
clearError();
} catch (error) {
// Error is handled in the store
}
};
const handleEditUser = async (userData: any) => {
if (!editingUser) return;
try {
await updateUser(editingUser.id, userData);
setEditingUser(null);
clearError();
} catch (error) {
// Error is handled in the store
}
};
const handleDeleteUser = async (user: any) => {
if (confirm(`Are you sure you want to delete ${user.email}?`)) {
try {
await deleteUser(user.id);
clearError();
} catch (error) {
// Error is handled in the store
}
}
};
const handleToggleUserStatus = async (user: any) => {
try {
await updateUser(user.id, { isActive: !user.isActive });
clearError();
} catch (error) {
// Error is handled in the store
}
};
const handleBulkAction = async (action: string, userIds: string[]) => {
const actionMessages = {
delete: `Are you sure you want to delete ${userIds.length} user(s)?`,
activate: `Are you sure you want to activate ${userIds.length} user(s)?`,
deactivate: `Are you sure you want to deactivate ${userIds.length} user(s)?`,
};
if (!confirm(actionMessages[action as keyof typeof actionMessages])) {
return;
}
try {
if (action === 'delete') {
await bulkDeleteUsers(userIds);
} else if (action === 'activate' || action === 'deactivate') {
const updates = userIds.map(id => ({
id,
role: users.find(u => u.id === id)?.role || 'USER',
}));
// Note: For status changes, we'd need a bulk update status method
// For now, we'll handle individual updates
for (const userId of userIds) {
const user = users.find(u => u.id === userId);
if (user) {
await updateUser(userId, { isActive: action === 'activate' });
}
}
}
clearError();
} catch (error) {
// Error is handled in the store
}
};
// Desktop Header
const desktopHeader = (
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="flex items-center space-x-2"
>
<ArrowLeft className="w-4 h-4" />
<span>Back</span>
</Button>
<h1 className="text-xl font-semibold text-white">Users Management</h1>
</div>
<Button
onClick={() => setShowCreateModal(true)}
className="flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span>Add User</span>
</Button>
</div>
);
// Mobile Header
const mobileHeader = (
<div className="flex items-center justify-between w-full px-4">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="p-2"
>
<ArrowLeft className="w-4 h-4 text-white" />
</Button>
<h1 className="text-lg font-semibold text-white">Users</h1>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowCreateModal(true)}
className="p-2"
>
<Plus className="w-5 h-5 text-white" />
</Button>
</div>
);
const pageContent = (
<div className="p-6 max-w-7xl mx-auto">
{/* Page Title (hidden on mobile, shown in header) */}
<div className="hidden md:block mb-8">
<h1 className="text-3xl font-bold text-gray-900">Users Management</h1>
<p className="text-gray-600 mt-2">
Manage user accounts, roles, and permissions
</p>
</div>
{/* Filters */}
<Card className="p-4 mb-6">
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<Input
placeholder="Search users..."
value={searchQuery}
onChange={setSearchQuery}
className="pl-10"
/>
</div>
</div>
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Roles</option>
<option value="USER">User</option>
<option value="ADMIN">Admin</option>
{isSuperAdmin && <option value="SUPER_ADMIN">Super Admin</option>}
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</Card>
{/* Error Display */}
{error && (
<Card className="p-4 mb-6 bg-red-50 border-red-200">
<div className="flex items-center justify-between">
<p className="text-red-800">{error}</p>
<Button variant="secondary" size="sm" onClick={clearError}>
Dismiss
</Button>
</div>
</Card>
)}
{/* Users Table */}
<UsersTable
users={users}
loading={isLoading}
onEditUser={setEditingUser}
onDeleteUser={handleDeleteUser}
onToggleUserStatus={handleToggleUserStatus}
onBulkAction={handleBulkAction}
showBulkActions={isSuperAdmin}
/>
{/* Pagination Info */}
{pagination && (
<div className="mt-6 text-center text-gray-600">
Showing {users.length} of {pagination.total} users
(Page {pagination.page} of {pagination.totalPages})
</div>
)}
{/* Create User Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="max-w-md w-full max-h-[90vh] overflow-y-auto">
<UserForm
isCreate
loading={isLoading}
onSubmit={handleCreateUser}
onCancel={() => setShowCreateModal(false)}
/>
</div>
</div>
)}
{/* Edit User Modal */}
{editingUser && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="max-w-md w-full max-h-[90vh] overflow-y-auto">
<UserForm
user={editingUser}
loading={isLoading}
onSubmit={handleEditUser}
onCancel={() => setEditingUser(null)}
/>
</div>
</div>
)}
</div>
);
return (
<AuthGuard requiredRoles={[Role.ADMIN, Role.SUPER_ADMIN]}>
<ResponsiveLayout
header={mobileHeader}
showHeader={true}
enablePullToRefresh={true}
onRefresh={async () => {
await fetchUsers();
}}
>
{pageContent}
</ResponsiveLayout>
</AuthGuard>
);
}

View File

@@ -1,120 +0,0 @@
'use client';
import { useEffect, useState, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAuthStore } from '@/stores/auth-store';
import { getStoredRedirectUrl } from '@/features/auth/lib/oauth';
/**
* EN: OAuth callback content component
* VI: Component nội dung OAuth callback
*/
function OAuthCallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { oauthLogin } = useAuthStore();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
/**
* EN: Handle OAuth callback with token from URL
* VI: Xử lý OAuth callback với token từ URL
*/
const handleCallback = async () => {
try {
// EN: Get token from URL query parameters
// VI: Lấy token từ query parameters của URL
const token = searchParams.get('token');
if (!token) {
throw new Error('No token provided / Không có token được cung cấp');
}
// EN: Authenticate with OAuth token
// VI: Xác thực với OAuth token
await oauthLogin(token);
// EN: Get stored redirect URL or default to home
// VI: Lấy redirect URL đã lưu hoặc mặc định về trang chủ
const redirectUrl = getStoredRedirectUrl() || '/';
// EN: Redirect to intended destination
// VI: Chuyển hướng đến đích dự định
router.push(redirectUrl);
} catch (err: any) {
console.error('OAuth callback error:', err);
setError(err.message || 'Authentication failed / Xác thực thất bại');
// EN: Redirect to error page after 3 seconds
// VI: Chuyển hướng đến trang lỗi sau 3 giây
setTimeout(() => {
router.push(`/auth/error?message=${encodeURIComponent(err.message || 'Authentication failed')}`);
}, 3000);
}
};
handleCallback();
}, [searchParams, router, oauthLogin]);
// EN: Loading state while processing callback
// VI: Trạng thái loading trong khi xử lý callback
if (!error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-lg">Completing authentication... / Đang hoàn tất xác thực...</p>
</div>
</div>
);
}
// EN: Error state
// VI: Trạng thái lỗi
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="text-red-500 mb-4">
<svg
className="w-16 h-16 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="text-lg text-red-500 mb-2">Error / Lỗi</p>
<p className="text-gray-600">{error}</p>
<p className="text-sm text-gray-500 mt-4">
Redirecting... / Đang chuyển hướng...
</p>
</div>
</div>
);
}
/**
* EN: OAuth callback page to handle authentication after OAuth redirect
* VI: Trang callback OAuth để xử lý xác thực sau khi redirect OAuth
*/
export default function OAuthCallbackPage() {
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-lg">Loading... / Đang tải...</p>
</div>
</div>
}
>
<OAuthCallbackContent />
</Suspense>
);
}

View File

@@ -1,108 +0,0 @@
'use client';
import { Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/features/shared/components/ui/button';
import { handleOAuthError } from '@/features/auth/lib/oauth';
/**
* EN: OAuth error content component
* VI: Component nội dung lỗi OAuth
*/
function OAuthErrorContent() {
const router = useRouter();
const searchParams = useSearchParams();
// EN: Get error message from URL
// VI: Lấy thông báo lỗi từ URL
const errorMessage = searchParams.get('message') || 'Unknown error / Lỗi không xác định';
const decodedError = handleOAuthError(errorMessage);
/**
* EN: Handle retry by redirecting to login page
* VI: Xử lý thử lại bằng cách chuyển hướng đến trang đăng nhập
*/
const handleRetry = () => {
router.push('/login');
};
/**
* EN: Handle go home redirect
* VI: Xử lý chuyển hướng về trang chủ
*/
const handleGoHome = () => {
router.push('/');
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 text-center">
{/* EN: Error icon / VI: Icon lỗi */}
<div className="text-red-500 mb-4">
<svg
className="w-16 h-16 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
{/* EN: Error title / VI: Tiêu đề lỗi */}
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Authentication Error / Lỗi Xác Thực
</h1>
{/* EN: Error message / VI: Thông báo lỗi */}
<p className="text-gray-600 dark:text-gray-300 mb-6">
{decodedError}
</p>
{/* EN: Action buttons / VI: Các nút hành động */}
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button
onClick={handleRetry}
variant="primary"
className="w-full sm:w-auto"
>
Try Again / Thử Lại
</Button>
<Button
onClick={handleGoHome}
variant="secondary"
className="w-full sm:w-auto"
>
Go Home / Về Trang Chủ
</Button>
</div>
</div>
</div>
);
}
/**
* EN: OAuth error page to display authentication errors
* VI: Trang lỗi OAuth để hiển thị lỗi xác thực
*/
export default function OAuthErrorPage() {
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-lg">Loading... / Đang tải...</p>
</div>
</div>
}
>
<OAuthErrorContent />
</Suspense>
);
}

View File

@@ -1,22 +0,0 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
/**
* EN: Redirect /auth/login to /login
* VI: Chuyển hướng /auth/login sang /login
*/
export default function AuthLoginRedirect() {
const router = useRouter();
useEffect(() => {
router.replace('/login');
}, [router]);
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-text-secondary">Redirecting...</p>
</div>
);
}

View File

@@ -1,332 +0,0 @@
'use client';
import React, { useState } from 'react';
import { MessageCircle, User, Settings, Home, Search, LogOut, Menu } from 'lucide-react';
import { useAuthStore } from '../../stores/auth-store';
import { ResponsiveLayout } from '../../features/shared/components/layout/responsive-layout';
import { MobileBottomNav, useBottomNav } from '../../features/shared/components/layout/mobile-layout';
import { Button } from '../../features/shared/components/ui/button';
import { Card } from '../../features/shared/components/ui/card';
import { useRouter } from 'next/navigation';
/**
* EN: Dashboard Page - Main authenticated user interface
* VI: Trang Dashboard - Giao diện chính cho người dùng đã xác thực
*
* Features:
* - Responsive layout (desktop sidebar + mobile bottom nav)
* - Quick actions and status overview
* - Navigation to different sections
* - User greeting and avatar
*/
export default function DashboardPage() {
const { user, logout } = useAuthStore();
const router = useRouter();
const { activeItem, handleNavPress } = useBottomNav('home');
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
if (!user) {
router.push('/auth/login');
return null;
}
const handleLogout = async () => {
await logout();
router.push('/');
};
// Desktop Header
const desktopHeader = (
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-2"
>
<Menu className="w-5 h-5" />
</Button>
<h1 className="text-xl font-semibold text-white">Dashboard</h1>
</div>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-semibold text-sm">
{user.email[0].toUpperCase()}
</span>
</div>
<Button variant="ghost" size="sm" onClick={handleLogout}>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button>
</div>
</div>
);
// Mobile Header
const mobileHeader = (
<div className="flex items-center justify-between w-full px-4">
<h1 className="text-lg font-semibold text-white">Dashboard</h1>
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-semibold text-sm">
{user.email[0].toUpperCase()}
</span>
</div>
</div>
</div>
);
// Desktop Sidebar
const desktopSidebar = (
<div className="p-4">
<div className="mb-8">
<div className="flex items-center space-x-3 mb-6">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-semibold">
{user.email[0].toUpperCase()}
</span>
</div>
{!sidebarCollapsed && (
<div>
<p className="font-medium text-gray-900">{user.email}</p>
<p className="text-sm text-gray-500 capitalize">{user.role.toLowerCase()}</p>
</div>
)}
</div>
{!sidebarCollapsed && (
<nav className="space-y-2">
{[
{ id: 'home', label: 'Home', icon: Home },
{ id: 'chat', label: 'Chat', icon: MessageCircle },
{ id: 'search', label: 'Search', icon: Search },
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'settings', label: 'Settings', icon: Settings },
].map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => {
if (item.id === 'chat') router.push('/chat');
else if (item.id === 'profile') router.push('/profile');
else if (item.id === 'settings') router.push('/settings');
}}
className="w-full flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<Icon className="w-5 h-5 text-gray-600" />
<span className="text-gray-700">{item.label}</span>
</button>
);
})}
</nav>
)}
</div>
{!sidebarCollapsed && (
<div className="border-t pt-4">
<Button
variant="secondary"
size="sm"
onClick={handleLogout}
className="w-full flex items-center justify-center"
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button>
</div>
)}
</div>
);
// Mobile Bottom Navigation Items
const mobileNavItems = [
{
id: 'home',
label: 'Home',
icon: <Home className="w-6 h-6" />,
},
{
id: 'search',
label: 'Search',
icon: <Search className="w-6 h-6" />,
},
{
id: 'chat',
label: 'Chat',
icon: <MessageCircle className="w-6 h-6" />,
badge: 2,
},
{
id: 'profile',
label: 'Profile',
icon: <User className="w-6 h-6" />,
},
{
id: 'settings',
label: 'Settings',
icon: <Settings className="w-6 h-6" />,
},
];
const handleMobileNavPress = (itemId: string) => {
handleNavPress(itemId);
if (itemId === 'chat') router.push('/chat');
else if (itemId === 'profile') router.push('/profile');
else if (itemId === 'settings') router.push('/settings');
};
// Dashboard Content
const dashboardContent = (
<div className="p-6 max-w-4xl mx-auto">
{/* Welcome Section */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Welcome back, {user.email.split('@')[0]}!
</h2>
<p className="text-gray-600">
Here's what's happening with your account today.
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<Card className="p-6">
<div className="flex items-center">
<div className="p-3 bg-blue-100 rounded-lg">
<MessageCircle className="w-6 h-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Active Chats</p>
<p className="text-2xl font-bold text-gray-900">3</p>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center">
<div className="p-3 bg-green-100 rounded-lg">
<User className="w-6 h-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Profile Views</p>
<p className="text-2xl font-bold text-gray-900">12</p>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center">
<div className="p-3 bg-purple-100 rounded-lg">
<Settings className="w-6 h-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Settings Updated</p>
<p className="text-2xl font-bold text-gray-900">2</p>
</div>
</div>
</Card>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div className="space-y-3">
<Button
onClick={() => router.push('/chat')}
className="w-full flex items-center justify-start"
variant="secondary"
>
<MessageCircle className="w-4 h-4 mr-3" />
Start New Chat
</Button>
<Button
onClick={() => router.push('/profile')}
className="w-full flex items-center justify-start"
variant="secondary"
>
<User className="w-4 h-4 mr-3" />
Update Profile
</Button>
<Button
onClick={() => router.push('/settings')}
className="w-full flex items-center justify-start"
variant="secondary"
>
<Settings className="w-4 h-4 mr-3" />
Account Settings
</Button>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<p className="text-sm text-gray-600">Started a new conversation</p>
<span className="text-xs text-gray-400">2h ago</span>
</div>
<div className="flex items-center space-x-3">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<p className="text-sm text-gray-600">Updated profile information</p>
<span className="text-xs text-gray-400">1d ago</span>
</div>
<div className="flex items-center space-x-3">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<p className="text-sm text-gray-600">Changed notification settings</p>
<span className="text-xs text-gray-400">3d ago</span>
</div>
</div>
</Card>
</div>
{/* Account Status */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Account Status</h3>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Account Active</p>
<p className="text-sm text-gray-600">
Your account is in good standing. Member since {new Date(user.createdAt).getFullYear()}.
</p>
</div>
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
user.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{user.isActive ? 'Active' : 'Inactive'}
</div>
</div>
</Card>
</div>
);
return (
<ResponsiveLayout
// Desktop props
header={desktopHeader}
sidebar={desktopSidebar}
showSidebar={true}
sidebarWidth={sidebarCollapsed ? 64 : 280}
sidebarCollapsible={true}
sidebarCollapsed={sidebarCollapsed}
// Mobile props
showBottomNav={true}
bottomNavItems={mobileNavItems}
activeNavItem={activeItem}
onNavItemPress={handleMobileNavPress}
enablePullToRefresh={true}
onRefresh={async () => {
// Simulate refresh
await new Promise(resolve => setTimeout(resolve, 1000));
}}
>
{dashboardContent}
</ResponsiveLayout>
);
}

View File

@@ -1,383 +0,0 @@
/**
* EN: Global Styles with Tailwind CSS 4
* VI: Styles toàn cục với Tailwind CSS 4
*
* Import theme variables first, then glassmorphism utilities, then Tailwind CSS 4
* Import các biến theme trước, sau đó là glassmorphism utilities, rồi đến Tailwind CSS 4
*/
@import "../styles/theme.css";
@import "../styles/glass.css";
@import "tailwindcss";
/**
* EN: Base Styles
* VI: Styles cơ bản
*/
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scroll-behavior: smooth;
}
/**
* EN: Disable smooth scroll for users who prefer reduced motion
* VI: Tắt smooth scroll cho người dùng ưa thích giảm chuyển động
*/
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
/* Also disable all animations */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/**
* EN: Custom text selection (X.ai blue)
* VI: Tùy chỉnh text selection (X.ai blue)
*/
::selection {
background-color: var(--accent-primary);
color: white;
}
::-moz-selection {
background-color: var(--accent-primary);
color: white;
}
/**
* EN: Custom scrollbar (X.ai minimal)
* VI: Tùy chỉnh scrollbar (X.ai minimal)
*/
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border-default);
border-radius: var(--radius-full);
transition: background var(--duration-fast) var(--ease-snap);
}
::-webkit-scrollbar-thumb:hover {
background: var(--glass-border-hover);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--glass-border-default) var(--bg-secondary);
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-size: var(--text-base);
font-weight: var(--font-light);
line-height: var(--leading-relaxed);
transition: background-color var(--duration-normal) var(--ease-in-out),
color var(--duration-normal) var(--ease-in-out);
}
/**
* EN: Smooth transitions for theme switching
* VI: Chuyển đổi mượt mà cho việc chuyển theme
*/
*,
*::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);
}
/**
* EN: Focus indicators for keyboard navigation (WCAG 2.1 AA)
* VI: Chỉ báo focus cho điều hướng bàn phím (WCAG 2.1 AA)
*/
*:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
}
/**
* EN: Skip link styles (screen reader only until focused)
* VI: Styles cho skip link (chỉ screen reader cho đến khi focus)
*/
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.sr-only:focus,
.sr-only:focus-visible {
position: fixed;
width: auto;
height: auto;
padding: inherit;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
/**
* EN: Ensure minimum font size for accessibility (16px) - WCAG 2.1 AA
* VI: Đảm bảo kích thước font tối thiểu cho accessibility (16px) - WCAG 2.1 AA
*/
body {
font-size: 16px;
}
}
/**
* EN: Message bubble fade-in animation
* VI: Animation fade-in cho message bubble
*/
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
filter: blur(10px);
}
to {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
}
/**
* EN: Floating Animation - X.ai Minimalist (Simplified)
* VI: Animation lơ lửng - X.ai Minimalist (Đơn giản hóa)
*/
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
/**
* EN: Skeleton Loading Animation
* VI: Animation loading skeleton
*/
@keyframes skeleton {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ============================================
EN: Removed - Mesh Gradient Animation (X.ai Minimalist)
VI: Đã xóa - Mesh Gradient Animation (X.ai Minimalist)
============================================ */
/* Removed for cleaner, minimalist aesthetic */
/* ============================================
EN: Removed - Shimmer Animation (X.ai Minimalist)
VI: Đã xóa - Shimmer Animation (X.ai Minimalist)
============================================ */
/* Removed for cleaner, minimalist aesthetic */
/**
* EN: Utilities Layer
* VI: Layer Utilities
*/
@layer utilities {
.animate-fadeIn {
animation: fadeIn 1s var(--ease-spring) forwards;
}
/* EN: Simplified float animation - X.ai style */
.animate-float {
animation: float 4s var(--ease-smooth) infinite;
}
/**
* EN: Skeleton Loading Utilities
* VI: Utilities loading skeleton
*/
.skeleton {
background: linear-gradient(90deg,
var(--glass-bg-subtle) 0%,
var(--glass-bg-default) 50%,
var(--glass-bg-subtle) 100%);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
border-radius: var(--radius-md);
}
.skeleton-text {
height: 1em;
margin-bottom: 0.5em;
}
.skeleton-title {
height: 2em;
margin-bottom: 1em;
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
}
.skeleton-card {
height: 200px;
border-radius: var(--radius-lg);
}
/**
* EN: Button Press Micro-interaction
* VI: Micro-interaction cho button press
*/
.btn-press {
transition: transform var(--duration-fast) var(--ease-snap);
}
.btn-press:active:not(:disabled) {
transform: scale(0.98);
}
/* ============================================
EN: Removed - Cosmic Effects (X.ai Minimalist)
VI: Đã xóa - Cosmic Effects (X.ai Minimalist)
============================================ */
/* Removed .mesh-gradient, .mesh-spot, .text-brand-glow, .animate-shimmer */
/* Use simple backgrounds and minimal effects instead */
.container-responsive {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 640px) {
.container-responsive {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container-responsive {
max-width: 768px;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.container-responsive {
max-width: 1024px;
padding-left: 2rem;
padding-right: 2rem;
}
}
@media (min-width: 1280px) {
.container-responsive {
max-width: 1280px;
}
}
.text-responsive-hero {
font-size: 2.5rem;
line-height: 1.1;
}
@media (min-width: 768px) {
.text-responsive-hero {
font-size: 3.75rem;
}
}
.btn-touch {
min-height: 44px;
min-width: 44px;
}
}
/**
* EN: Print Styles
* VI: Styles cho in ấn
*/
@media print {
body {
background: white;
color: black;
}
.glass-card {
background: white;
border: 1px solid #ccc;
box-shadow: none;
}
/* Hide non-essential elements */
nav,
footer,
.no-print,
button,
.auth-controls {
display: none !important;
}
/* Ensure readable text */
* {
color: black !important;
background: white !important;
}
a {
text-decoration: underline;
}
/* Show link URLs after text */
a[href]:after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #666;
}
}

View File

@@ -1,92 +0,0 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ThemeProvider } from '../features/theme';
import { QueryProvider } from '../providers/query-provider';
import { I18nProvider } from '../features/theme/i18n-provider';
// EN: Configure Inter font with subsets and variable name
// VI: Cấu hình font Inter với các subsets và tên biến CSS
const inter = Inter({
subsets: ['latin', 'vietnamese'],
display: 'swap',
variable: '--font-inter',
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
});
import { SkipToContent } from '../features/shared/components/accessibility/skip-to-content';
/**
* EN: Metadata for the application
* VI: Metadata cho ứng dụng
*/
export const metadata: Metadata = {
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 (Next.js 13+ requires separate export)
* VI: Cấu hình viewport (Next.js 13+ yêu cầu export riêng)
*/
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 5,
};
/**
* EN: Root layout component for the entire application
* VI: Component layout gốc cho toàn bộ ứng dụng
*
* @param children - Child components to render / Components con để render
*/
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
// EN: Root HTML structure with dynamic language (will be updated by I18nProvider)
// VI: Cấu trúc HTML gốc với ngôn ngữ động (sẽ được cập nhật bởi I18nProvider)
<html lang="en" className={inter.variable} suppressHydrationWarning>
<body>
<I18nProvider>
<SkipToContent />
<QueryProvider>
<ThemeProvider>{children}</ThemeProvider>
</QueryProvider>
</I18nProvider>
</body>
</html>
);
}

View File

@@ -1,39 +0,0 @@
'use client';
import { MobileAppDemo } from '@/features/shared/components/layout/mobile-layout';
/**
* EN: Mobile App Demo Page
* VI: Trang Demo Mobile App
*
* Showcases the native app-style mobile layout components.
* Trình diễn các component mobile layout theo phong cách native app.
*/
export default function MobileDemoPage() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Mobile App Layout Demo
</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
Experience native app-style mobile layouts with pull-to-refresh,
bottom navigation, and touch-optimized interactions.
</p>
</div>
<div className="flex justify-center">
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden">
<MobileAppDemo />
</div>
</div>
<div className="mt-8 text-center text-sm text-gray-500">
<p>Resize your browser window to mobile size to see the full experience!</p>
<p className="mt-2">Try pulling down on the Home screen to test pull-to-refresh.</p>
</div>
</div>
</div>
);
}

View File

@@ -1,56 +0,0 @@
/**
* EN: Mood Board - Design System Showcase
* VI: Mood Board - Showcase Design System
*
* A comprehensive showcase of all UI components and design tokens
* Một trang showcase toàn diện cho tất cả UI components và design tokens
*/
import { Metadata } from 'next';
import { ThemeToggle } from '@/features/theme/components/theme-toggle-enhanced';
import TypographySection from './sections/TypographySection';
import ColorPaletteSection from './sections/ColorPaletteSection';
import SpacingSection from './sections/SpacingSection';
import GlassEffectsSection from './sections/GlassEffectsSection';
import ButtonsSection from './sections/ButtonsSection';
export const metadata: Metadata = {
title: 'Mood Board | Design System Showcase',
description: 'Comprehensive showcase of all UI components and design tokens',
};
export default function MoodBoardPage() {
return (
<div className="min-h-screen bg-bg-primary">
{/* EN: Header with sticky navigation / VI: Header với navigation sticky */}
<header className="glass-nav sticky top-0 z-50 p-6">
<div className="container-responsive flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold tracking-tight text-text-primary">
Mood Board
</h1>
<p className="text-text-secondary mt-2">
Design System Showcase - All Components & Tokens
</p>
</div>
{/* EN: Theme Toggle / VI: Theme Toggle */}
<ThemeToggle />
</div>
</header>
{/* EN: Main content with all sections / VI: Nội dung chính với tất cả sections */}
<main className="container-responsive py-12 space-y-16">
<TypographySection />
<ColorPaletteSection />
<SpacingSection />
<GlassEffectsSection />
<ButtonsSection />
</main>
{/* EN: Footer / VI: Footer */}
<footer className="border-t border-border-primary p-6 text-center text-text-tertiary">
<p>Design System v1.0.0 - X.ai Minimalist Theme</p>
</footer>
</div>
);
}

View File

@@ -1,113 +0,0 @@
/**
* EN: Buttons Section - All button variants
* VI: Buttons Section - Tất cả button variants
*/
import SectionWrapper from '@/features/mood-board/components/SectionWrapper';
import ComponentShowcase from '@/features/mood-board/components/ComponentShowcase';
export default function ButtonsSection() {
return (
<SectionWrapper
id="buttons"
title="Buttons"
description="All button styles and states"
>
{/* EN: Glass Buttons / VI: Glass Buttons */}
<ComponentShowcase
title="Glass Buttons"
description="Glassmorphism button styles"
code={`<button className="glass-button">Default Glass</button>
<button className="glass-button" disabled>Disabled</button>`}
darkBg
>
<div className="flex gap-4 flex-wrap">
<button className="glass-button">Default Glass</button>
<button className="glass-button hover:bg-glass-bg-hover">
Hover Me
</button>
<button className="glass-button" disabled>
Disabled
</button>
</div>
</ComponentShowcase>
{/* EN: Primary Buttons / VI: Primary Buttons */}
<ComponentShowcase
title="Primary Buttons (Accent)"
description="Brand primary button with X.ai blue"
code={`<button className="bg-accent-primary text-white px-6 py-3 rounded-md font-medium hover:bg-accent-primary-hover transition-all">
Primary Action
</button>`}
>
<div className="flex gap-4 flex-wrap">
<button className="bg-accent-primary text-white px-6 py-3 rounded-md font-medium hover:bg-accent-primary-hover transition-all btn-press">
Primary Action
</button>
<button className="bg-accent-primary text-white px-6 py-3 rounded-md font-medium opacity-50 cursor-not-allowed">
Disabled Primary
</button>
</div>
</ComponentShowcase>
{/* EN: Secondary Buttons / VI: Secondary Buttons */}
<ComponentShowcase
title="Secondary Buttons"
description="Secondary button style with border"
code={`<button className="border border-border-primary text-text-primary px-6 py-3 rounded-md font-medium hover:border-border-secondary transition-all">
Secondary Action
</button>`}
>
<div className="flex gap-4 flex-wrap">
<button className="border border-border-primary text-text-primary px-6 py-3 rounded-md font-medium hover:border-border-secondary hover:bg-bg-secondary transition-all btn-press">
Secondary Action
</button>
<button className="border border-border-primary text-text-primary px-6 py-3 rounded-md font-medium opacity-50 cursor-not-allowed">
Disabled Secondary
</button>
</div>
</ComponentShowcase>
{/* EN: Button Sizes / VI: Button Sizes */}
<ComponentShowcase
title="Button Sizes"
description="Different button size variants"
code={`<button className="glass-button text-xs px-3 py-1.5">Small</button>
<button className="glass-button text-sm px-4 py-2">Medium</button>
<button className="glass-button text-base px-6 py-3">Large</button>`}
darkBg
>
<div className="flex gap-4 items-center flex-wrap">
<button className="glass-button text-xs px-3 py-1.5">Small</button>
<button className="glass-button text-sm px-4 py-2">Medium</button>
<button className="glass-button text-base px-6 py-3">Large</button>
<button className="glass-button text-lg px-8 py-4">Extra Large</button>
</div>
</ComponentShowcase>
{/* EN: Icon Buttons / VI: Icon Buttons */}
<ComponentShowcase
title="Icon Buttons"
description="Square icon buttons for actions"
code={`<button className="glass-button w-10 h-10 flex items-center justify-center">
×
</button>`}
darkBg
>
<div className="flex gap-4 flex-wrap">
<button className="glass-button w-10 h-10 flex items-center justify-center">
×
</button>
<button className="glass-button w-10 h-10 flex items-center justify-center">
</button>
<button className="glass-button w-10 h-10 flex items-center justify-center">
</button>
<button className="glass-button w-12 h-12 flex items-center justify-center rounded-full">
</button>
</div>
</ComponentShowcase>
</SectionWrapper>
);
}

View File

@@ -1,83 +0,0 @@
/**
* EN: Color Palette Section - All design token colors
* VI: Color Palette Section - Tất cả color tokens từ design system
*/
import SectionWrapper from '@/features/mood-board/components/SectionWrapper';
import ColorSwatch from '@/features/mood-board/components/ColorSwatch';
export default function ColorPaletteSection() {
return (
<SectionWrapper
id="colors"
title="Color Palette"
description="All color tokens - backgrounds, text, borders, accents (Dark/Light mode)"
>
{/* EN: Background Colors / VI: Màu nền */}
<div>
<h3 className="text-xl font-semibold mb-4 text-text-primary">
Background Colors
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<ColorSwatch name="--bg-primary" var="var(--bg-primary)" />
<ColorSwatch name="--bg-secondary" var="var(--bg-secondary)" />
<ColorSwatch name="--bg-tertiary" var="var(--bg-tertiary)" />
<ColorSwatch name="--bg-elevated" var="var(--bg-elevated)" />
</div>
</div>
{/* EN: Text Colors / VI: Màu chữ */}
<div>
<h3 className="text-xl font-semibold mb-4 text-text-primary">
Text Colors (WCAG Compliant)
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<ColorSwatch name="--text-primary" var="var(--text-primary)" />
<ColorSwatch name="--text-secondary" var="var(--text-secondary)" />
<ColorSwatch name="--text-tertiary" var="var(--text-tertiary)" />
<ColorSwatch name="--text-muted" var="var(--text-muted)" />
</div>
</div>
{/* EN: Accent Colors / VI: Màu accent */}
<div>
<h3 className="text-xl font-semibold mb-4 text-text-primary">
Accent Colors (X.ai Brand)
</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<ColorSwatch name="--accent-primary" var="var(--accent-primary)" />
<ColorSwatch name="--accent-success" var="var(--accent-success)" />
<ColorSwatch name="--accent-warning" var="var(--accent-warning)" />
<ColorSwatch name="--accent-error" var="var(--accent-error)" />
<ColorSwatch name="--accent-info" var="var(--accent-info)" />
</div>
</div>
{/* EN: Border Colors / VI: Màu viền */}
<div>
<h3 className="text-xl font-semibold mb-4 text-text-primary">
Border Colors
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<ColorSwatch name="--border-primary" var="var(--border-primary)" />
<ColorSwatch
name="--border-secondary"
var="var(--border-secondary)"
/>
<ColorSwatch name="--border-focus" var="var(--border-focus)" />
</div>
</div>
{/* EN: Chat Colors / VI: Màu chat */}
<div>
<h3 className="text-xl font-semibold mb-4 text-text-primary">
Chat Specific Colors
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<ColorSwatch name="--chat-user-bubble" var="var(--chat-user-bubble)" />
<ColorSwatch name="--chat-user-text" var="var(--chat-user-text)" />
<ColorSwatch name="--chat-ai-text" var="var(--chat-ai-text)" />
</div>
</div>
</SectionWrapper>
);
}

View File

@@ -1,133 +0,0 @@
/**
* EN: Glass Effects Section - All glassmorphism utilities
* VI: Glass Effects Section - Tất cả glassmorphism utilities
*/
import SectionWrapper from '@/features/mood-board/components/SectionWrapper';
import ComponentShowcase from '@/features/mood-board/components/ComponentShowcase';
export default function GlassEffectsSection() {
return (
<SectionWrapper
id="glass-effects"
title="Glassmorphism Effects"
description="All glass utilities from glass.css (X.ai Minimal Style)"
>
{/* EN: Glass Card / VI: Glass Card */}
<ComponentShowcase
title="Glass Card"
description="Default glass card with subtle blur and border"
code={`<div className="glass-card p-6">
<h3 className="text-xl font-semibold">Glass Card</h3>
<p>Ultra-minimal glassmorphism effect</p>
</div>`}
darkBg
>
<div className="glass-card p-6 max-w-md">
<h3 className="text-xl font-semibold text-text-primary mb-2">
Glass Card
</h3>
<p className="text-text-secondary">
Ultra-minimal glassmorphism effect with 4% opacity background and 8px
blur.
</p>
</div>
</ComponentShowcase>
{/* EN: Glass Variants / VI: Glass Variants */}
<ComponentShowcase
title="Glass Variants"
description="Different glass effect intensities"
code={`<div className="glass-subtle">Subtle (1% bg, 4px blur)</div>
<div className="glass-card">Default (4% bg, 8px blur)</div>
<div className="glass-strong">Strong (5% bg, 12px blur)</div>`}
darkBg
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="glass-subtle p-6 rounded-lg border border-glass-border-subtle">
<h4 className="font-semibold text-text-primary mb-2">
Glass Subtle
</h4>
<p className="text-sm text-text-secondary">
1% bg, 4px blur
</p>
</div>
<div className="glass-card p-6">
<h4 className="font-semibold text-text-primary mb-2">
Glass Card (Default)
</h4>
<p className="text-sm text-text-secondary">
4% bg, 8px blur
</p>
</div>
<div className="glass-strong p-6 rounded-lg">
<h4 className="font-semibold text-text-primary mb-2">
Glass Strong
</h4>
<p className="text-sm text-text-secondary">
5% bg, 12px blur
</p>
</div>
</div>
</ComponentShowcase>
{/* EN: Glass Button / VI: Glass Button */}
<ComponentShowcase
title="Glass Button"
description="Interactive glass button with hover/active states"
code={`<button className="glass-button">
Click Me
</button>`}
darkBg
>
<div className="flex gap-4 flex-wrap">
<button className="glass-button">Default Button</button>
<button className="glass-button" disabled>
Disabled Button
</button>
<button className="glass-button px-6 py-3">Larger Button</button>
</div>
</ComponentShowcase>
{/* EN: Glass Input / VI: Glass Input */}
<ComponentShowcase
title="Glass Input"
description="Glass input with focus states"
code={`<input
type="text"
placeholder="Enter text..."
className="glass-input w-full rounded-md px-4 py-2"
/>`}
darkBg
>
<div className="space-y-4 max-w-md">
<input
type="text"
placeholder="Glass input..."
className="glass-input w-full rounded-md px-4 py-2 text-text-primary"
/>
<input
type="email"
placeholder="Email address..."
className="glass-input w-full rounded-md px-4 py-2 text-text-primary"
/>
</div>
</ComponentShowcase>
{/* EN: Glass Badge / VI: Glass Badge */}
<ComponentShowcase
title="Glass Badge"
description="Small glass badges for tags and labels"
code={`<span className="glass-badge">Design</span>
<span className="glass-badge">Development</span>`}
darkBg
>
<div className="flex gap-2 flex-wrap">
<span className="glass-badge">Design</span>
<span className="glass-badge">Development</span>
<span className="glass-badge">X.ai Minimal</span>
<span className="glass-badge">Glassmorphism</span>
</div>
</ComponentShowcase>
</SectionWrapper>
);
}

View File

@@ -1,95 +0,0 @@
/**
* EN: Spacing Section - Spacing scale and layout tokens
* VI: Spacing Section - Spacing scale và layout tokens
*/
import SectionWrapper from '@/features/mood-board/components/SectionWrapper';
import ComponentShowcase from '@/features/mood-board/components/ComponentShowcase';
export default function SpacingSection() {
return (
<SectionWrapper
id="spacing"
title="Spacing & Layout"
description="Spacing scale (4px base unit) and layout tokens"
>
{/* EN: Spacing Scale / VI: Spacing Scale */}
<ComponentShowcase
title="Spacing Scale"
description="8-point grid system from --space-0 to --space-20"
code={`<div className="space-y-1">0.25rem (4px)</div>
<div className="space-y-2">0.5rem (8px)</div>
<div className="space-y-4">1rem (16px)</div>
<div className="space-y-8">2rem (32px)</div>`}
>
<div className="space-y-4">
{[
{ name: '--space-0', value: '0', px: '0px' },
{ name: '--space-1', value: '0.25rem', px: '4px' },
{ name: '--space-2', value: '0.5rem', px: '8px' },
{ name: '--space-3', value: '0.75rem', px: '12px' },
{ name: '--space-4', value: '1rem', px: '16px' },
{ name: '--space-5', value: '1.25rem', px: '20px' },
{ name: '--space-6', value: '1.5rem', px: '24px' },
{ name: '--space-8', value: '2rem', px: '32px' },
{ name: '--space-10', value: '2.5rem', px: '40px' },
{ name: '--space-12', value: '3rem', px: '48px' },
{ name: '--space-16', value: '4rem', px: '64px' },
{ name: '--space-20', value: '5rem', px: '80px' },
].map((token) => (
<div key={token.name} className="flex items-center gap-4">
<div
className="bg-accent-primary rounded"
style={{
width: token.value,
height: '24px',
minWidth: '4px',
}}
/>
<div>
<code className="text-sm font-mono text-text-primary">
{token.name}
</code>
<span className="text-text-tertiary text-sm ml-2">
{token.value} ({token.px})
</span>
</div>
</div>
))}
</div>
</ComponentShowcase>
{/* EN: Border Radius / VI: Border Radius */}
<ComponentShowcase
title="Border Radius"
description="Minimal roundness for X.ai aesthetic"
code={`<div className="rounded-sm">2px - Sharp</div>
<div className="rounded-md">4px - Buttons</div>
<div className="rounded-lg">8px - Cards</div>
<div className="rounded-full">9999px - Avatars</div>`}
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{[
{ name: '--radius-sm', value: '2px', class: 'rounded-sm' },
{ name: '--radius-md', value: '4px', class: 'rounded-md' },
{ name: '--radius-lg', value: '8px', class: 'rounded-lg' },
{ name: '--radius-xl', value: '12px', class: 'rounded-xl' },
{ name: '--radius-2xl', value: '16px', class: 'rounded-2xl' },
{ name: '--radius-full', value: '9999px', class: 'rounded-full' },
].map((radius) => (
<div key={radius.name} className="glass-card p-4">
<div
className={`bg-accent-primary w-full h-20 ${radius.class} mb-2`}
/>
<p className="text-sm font-medium text-text-primary">
{radius.name}
</p>
<code className="text-xs text-text-tertiary font-mono">
{radius.value}
</code>
</div>
))}
</div>
</ComponentShowcase>
</SectionWrapper>
);
}

View File

@@ -1,101 +0,0 @@
/**
* EN: Typography Section - Font scales, weights, letter spacing showcase
* VI: Typography Section - Showcase font scales, weights, letter spacing
*/
import SectionWrapper from '@/features/mood-board/components/SectionWrapper';
import ComponentShowcase from '@/features/mood-board/components/ComponentShowcase';
export default function TypographySection() {
return (
<SectionWrapper
id="typography"
title="Typography"
description="Font scales, weights, line heights, and letter spacing from Design System"
>
{/* EN: Font Sizes / VI: Font Sizes */}
<ComponentShowcase
title="Type Scale"
description="All font sizes from --text-xs (12px) to --text-6xl (56px)"
code={`<h1 className="text-6xl font-extrabold">Hero Title (56px)</h1>
<h2 className="text-5xl font-bold">Page Title (44px)</h2>
<h3 className="text-4xl font-semibold">Section Header (36px)</h3>
<p className="text-base">Body Text (16px)</p>
<span className="text-xs">Caption (12px)</span>`}
>
<div className="space-y-4">
<p className="text-6xl font-extrabold text-text-primary">
Hero Title (--text-6xl)
</p>
<p className="text-5xl font-bold text-text-primary">
Page Title (--text-5xl)
</p>
<p className="text-4xl font-semibold text-text-primary">
Section Header (--text-4xl)
</p>
<p className="text-3xl font-semibold text-text-primary">
Card Header (--text-3xl)
</p>
<p className="text-2xl text-text-primary">Large Body (--text-2xl)</p>
<p className="text-xl text-text-primary">Emphasized (--text-xl)</p>
<p className="text-lg text-text-primary">Large Body (--text-lg)</p>
<p className="text-base text-text-primary">
Default Body (--text-base)
</p>
<p className="text-sm text-text-secondary">Small Text (--text-sm)</p>
<p className="text-xs text-text-tertiary">Caption (--text-xs)</p>
</div>
</ComponentShowcase>
{/* EN: Font Weights / VI: Font Weights */}
<ComponentShowcase
title="Font Weights"
description="All weight variants for typography hierarchy (100-900)"
code={`<p className="font-thin">Thin (100)</p>
<p className="font-light">Light (300)</p>
<p className="font-normal">Normal (400)</p>
<p className="font-medium">Medium (500)</p>
<p className="font-semibold">Semibold (600)</p>
<p className="font-bold">Bold (700)</p>
<p className="font-extrabold">Extra Bold (800)</p>
<p className="font-black">Black (900)</p>`}
>
<div className="space-y-2 text-2xl text-text-primary">
<p className="font-thin">Thin (--font-thin: 100)</p>
<p className="font-extralight">
Extra Light (--font-extralight: 200)
</p>
<p className="font-light">Light (--font-light: 300)</p>
<p className="font-normal">Normal (--font-normal: 400)</p>
<p className="font-medium">Medium (--font-medium: 500)</p>
<p className="font-semibold">Semibold (--font-semibold: 600)</p>
<p className="font-bold">Bold (--font-bold: 700)</p>
<p className="font-extrabold">Extra Bold (--font-extrabold: 800)</p>
<p className="font-black">Black (--font-black: 900)</p>
</div>
</ComponentShowcase>
{/* EN: Letter Spacing / VI: Letter Spacing */}
<ComponentShowcase
title="Letter Spacing (Tracking)"
description="Tracking variants for clean, minimal aesthetics"
code={`<p className="tracking-tighter">Very Tight (-0.04em)</p>
<p className="tracking-tight">Tight (-0.02em)</p>
<p className="tracking-normal">Normal (0)</p>
<p className="tracking-wide">Wide (0.02em)</p>
<p className="tracking-wider">Wider (0.04em)</p>`}
>
<div className="space-y-2 text-2xl text-text-primary">
<p className="tracking-tighter">
Very Tight (--tracking-tighter: -0.04em)
</p>
<p className="tracking-tight">
Tight (--tracking-tight: -0.02em)
</p>
<p className="tracking-normal">Normal (--tracking-normal: 0)</p>
<p className="tracking-wide">Wide (--tracking-wide: 0.02em)</p>
<p className="tracking-wider">Wider (--tracking-wider: 0.04em)</p>
</div>
</ComponentShowcase>
</SectionWrapper>
);
}

View File

@@ -1,332 +0,0 @@
'use client';
import { useAuthStore } from '@/stores/auth-store';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import NextImage from 'next/image';
import { Button } from '@/ui';
import { Footer } from '@/shared/components/layout/footer';
import { NavigationHeader } from '@/shared/components/layout/header';
import { AnnouncementBanner } from '@/shared/components/layout/announcement-banner';
import {
ArrowRight,
Target, // Marketing/Ads
Gift, // Reward Apps
Store, // POS System
Cpu, // AI/Blockchain
BarChart3,
Smartphone,
CreditCard,
Blocks
} from 'lucide-react';
/**
* EN: Home page component - GoodGo Platform Landing Page
* VI: Component trang chủ - Trang đích GoodGo Platform
*/
export default function Home() {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Get authentication state from store
// VI: Lấy trạng thái xác thực từ store
const { user, isAuthenticated, isLoading, fetchUser } = useAuthStore();
// EN: State to track if component is mounted on client
// VI: State để theo dõi xem component đã mount trên client chưa
const [isMounted, setIsMounted] = useState(false);
// EN: Set mounted state to true on client side
// VI: Đặt trạng thái mounted thành true ở phía client
useEffect(() => {
setIsMounted(true);
}, []);
// EN: Fetch user data on component mount if not authenticated
// VI: Fetch dữ liệu user khi component mount nếu chưa xác thực
useEffect(() => {
if (isMounted && !isAuthenticated) {
fetchUser();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted]);
// EN: Prevent hydration mismatch
// VI: Ngăn chặn hydration mismatch
if (!isMounted) {
return null;
}
// EN: Loading state
// VI: Trạng thái loading
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 (
<>
<NavigationHeader />
{/* Announcement Banner */}
<AnnouncementBanner
title="GoodGo Enterprise"
description="Empowering businesses with comprehensive digital solutions"
linkText="Explore Now"
linkUrl="/services"
imageSrc="/images/home/announcement-bg.png"
/>
<main className="min-h-screen bg-bg-primary text-text-primary overflow-hidden relative">
{/* Ambient Background Effects - Removed for Monochrome */}
<div className="container mx-auto px-6 relative z-10">
{/* Hero Section */}
<div className="flex flex-col items-center justify-center min-h-[80vh] text-center space-y-10 pt-20">
<div className="max-w-5xl space-y-6">
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-glass-subtle border border-glass-border-subtle mb-4">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-text-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-text-primary"></span>
</span>
<span className="text-sm font-medium text-text-secondary">Ecosystem v2.0 Live</span>
</div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold leading-tight tracking-tight">
<span className="text-text-primary block">
Powering the Next
</span>
<span className="text-text-primary block">
Generation of Business
</span>
</h1>
<p className="text-xl md:text-2xl text-text-secondary max-w-3xl mx-auto leading-relaxed">
Comprehensive solutions for Marketing, Loyalty, Retail, and Web3 technologies.
Everything you need to grow your digital presence.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8">
<Button
size="lg"
className="rounded-full px-8 py-6 text-lg bg-text-primary text-bg-primary hover:bg-text-secondary w-full sm:w-auto transition-all shadow-glow"
onPress={() => window.location.href = '/contact'}
>
Start Building
</Button>
<Button
variant="glass"
size="lg"
className="rounded-full px-8 py-6 text-lg border-border-primary hover:bg-glass-hover w-full sm:w-auto backdrop-blur-md text-text-primary"
onPress={() => window.location.href = '/solutions'}
>
View Solutions
</Button>
</div>
</div>
</div>
{/* Solutions Section (4 Pillars) */}
<section className="py-24" id="solutions">
<div className="text-center mb-20 animate-fade-in-up">
<span className="text-text-tertiary font-bold tracking-wider uppercase text-sm mb-2 block">Our Ecosystem</span>
<h2 className="text-4xl md:text-5xl font-bold mb-4">Core Business Pillars</h2>
<p className="text-text-secondary max-w-2xl mx-auto text-lg">
Integrated technological pillars designed to scale your business operations from end to end.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Pillar 1: Marketing/Ads */}
<div className="group relative">
<div className="absolute -inset-0.5 bg-gradient-to-br from-text-secondary to-text-tertiary rounded-2xl opacity-10 group-hover:opacity-30 blur transition duration-500"></div>
<div className="relative h-full bg-bg-secondary/40 backdrop-blur-xl rounded-2xl border border-glass-border overflow-hidden hover:bg-bg-elevated/90 transition-all duration-300 flex flex-col justify-between">
<div>
<div className="relative h-48 w-full overflow-hidden">
<NextImage
src="/images/home/marketing-ads.png"
alt="Marketing & Ads"
fill
className="object-cover transition-transform duration-700 group-hover:scale-110 grayscale"
/>
<div className="absolute inset-0 bg-gradient-to-t from-bg-secondary via-transparent to-transparent"></div>
</div>
<div className="p-8 pt-6">
<div className="w-12 h-12 bg-glass-bg-hover rounded-xl flex items-center justify-center mb-4 text-text-primary">
<Target className="h-6 w-6" />
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-text-primary transition-colors">Marketing & Ads</h3>
<p className="text-text-tertiary leading-relaxed mb-6">
Advanced AdTech solutions, Real-Time Bidding (RTB), and cross-platform campaign management.
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-center text-sm text-text-secondary">
<BarChart3 className="h-4 w-4 mr-2 text-text-primary" /> Analytics
</li>
<li className="flex items-center text-sm text-text-secondary">
<Target className="h-4 w-4 mr-2 text-text-primary" /> Precise Targeting
</li>
</ul>
</div>
</div>
<div className="px-8 pb-8">
<Button variant="ghost" className="w-full justify-between group-hover:bg-glass-bg-hover group-hover:text-text-primary">
Learn more <ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* Pillar 2: Reward Apps */}
<div className="group relative">
<div className="absolute -inset-0.5 bg-gradient-to-br from-text-secondary to-text-tertiary rounded-2xl opacity-10 group-hover:opacity-30 blur transition duration-500"></div>
<div className="relative h-full bg-bg-secondary/40 backdrop-blur-xl rounded-2xl border border-glass-border overflow-hidden hover:bg-bg-elevated/90 transition-all duration-300 flex flex-col justify-between">
<div>
<div className="relative h-48 w-full overflow-hidden">
<NextImage
src="/images/home/reward-apps.png"
alt="Reward Apps"
fill
className="object-cover transition-transform duration-700 group-hover:scale-110 grayscale"
/>
<div className="absolute inset-0 bg-gradient-to-t from-bg-secondary via-transparent to-transparent"></div>
</div>
<div className="p-8 pt-6">
<div className="w-12 h-12 bg-glass-bg-hover rounded-xl flex items-center justify-center mb-4 text-text-primary">
<Gift className="h-6 w-6" />
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-text-primary transition-colors">Reward Apps</h3>
<p className="text-text-tertiary leading-relaxed mb-6">
Customer loyalty programs, gamification layers, and engagement ecosystems.
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-center text-sm text-text-secondary">
<Smartphone className="h-4 w-4 mr-2 text-text-primary" /> Mobile First
</li>
<li className="flex items-center text-sm text-text-secondary">
<Gift className="h-4 w-4 mr-2 text-text-primary" /> Loyalty Points
</li>
</ul>
</div>
</div>
<div className="px-8 pb-8">
<Button variant="ghost" className="w-full justify-between group-hover:bg-glass-bg-hover group-hover:text-text-primary">
Learn more <ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* Pillar 3: POS System */}
<div className="group relative">
<div className="absolute -inset-0.5 bg-gradient-to-br from-text-secondary to-text-tertiary rounded-2xl opacity-10 group-hover:opacity-30 blur transition duration-500"></div>
<div className="relative h-full bg-bg-secondary/40 backdrop-blur-xl rounded-2xl border border-glass-border overflow-hidden hover:bg-bg-elevated/90 transition-all duration-300 flex flex-col justify-between">
<div>
<div className="relative h-48 w-full overflow-hidden">
<NextImage
src="/images/home/pos-system.png"
alt="POS System"
fill
className="object-cover transition-transform duration-700 group-hover:scale-110 grayscale"
/>
<div className="absolute inset-0 bg-gradient-to-t from-bg-secondary via-transparent to-transparent"></div>
</div>
<div className="p-8 pt-6">
<div className="w-12 h-12 bg-glass-bg-hover rounded-xl flex items-center justify-center mb-4 text-text-primary">
<Store className="h-6 w-6" />
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-text-primary transition-colors">POS System</h3>
<p className="text-text-tertiary leading-relaxed mb-6">
Multi-industry Point of Sale solutions for Retail, F&B, and Service sectors.
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-center text-sm text-text-secondary">
<CreditCard className="h-4 w-4 mr-2 text-text-primary" /> Payments
</li>
<li className="flex items-center text-sm text-text-secondary">
<Store className="h-4 w-4 mr-2 text-text-primary" /> Inventory
</li>
</ul>
</div>
</div>
<div className="px-8 pb-8">
<Button variant="ghost" className="w-full justify-between group-hover:bg-glass-bg-hover group-hover:text-text-primary">
Learn more <ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* Pillar 4: AI/Blockchain */}
<div className="group relative">
<div className="absolute -inset-0.5 bg-gradient-to-br from-text-secondary to-text-tertiary rounded-2xl opacity-10 group-hover:opacity-30 blur transition duration-500"></div>
<div className="relative h-full bg-bg-secondary/40 backdrop-blur-xl rounded-2xl border border-glass-border overflow-hidden hover:bg-bg-elevated/90 transition-all duration-300 flex flex-col justify-between">
<div>
<div className="relative h-48 w-full overflow-hidden">
<NextImage
src="/images/home/ai-blockchain.png"
alt="AI & Blockchain"
fill
className="object-cover transition-transform duration-700 group-hover:scale-110 grayscale"
/>
<div className="absolute inset-0 bg-gradient-to-t from-bg-secondary via-transparent to-transparent"></div>
</div>
<div className="p-8 pt-6">
<div className="w-12 h-12 bg-glass-bg-hover rounded-xl flex items-center justify-center mb-4 text-text-primary">
<Cpu className="h-6 w-6" />
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-text-primary transition-colors">AI & Blockchain</h3>
<p className="text-text-tertiary leading-relaxed mb-6">
Next-gen infrastructure including Wallet, Mining, Mission, and AI-driven analytics.
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-center text-sm text-text-secondary">
<Blocks className="h-4 w-4 mr-2 text-text-primary" /> Web3 Native
</li>
<li className="flex items-center text-sm text-text-secondary">
<Cpu className="h-4 w-4 mr-2 text-text-primary" /> AI Insights
</li>
</ul>
</div>
</div>
<div className="px-8 pb-8">
<Button variant="ghost" className="w-full justify-between group-hover:bg-glass-bg-hover group-hover:text-text-primary">
Learn more <ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</section>
{/* Authenticated User Welcome - Kept from original but styled */}
{isAuthenticated && user && (
<section className="py-12 border-t border-border-primary">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-2xl font-bold mb-6">Ready to continue your work?</h2>
<div className="flex justify-center">
<Button
variant="solid"
className="bg-text-primary text-bg-primary hover:bg-text-secondary px-8"
onPress={() => window.location.href = '/dashboard'}
>
Go to Dashboard
</Button>
</div>
</div>
</section>
)}
</div>
</main>
<Footer />
</>
);
}

View File

@@ -1,332 +0,0 @@
'use client';
import React, { useState } from 'react';
import { User, Mail, Lock, Bell, Palette, Globe } from 'lucide-react';
import { useAuthStore } from '../../stores/auth-store';
import { UserForm } from '../../features/shared/components/users';
import { Button } from '../../features/shared/components/ui/button';
import { Card } from '../../features/shared/components/ui/card';
import { Switch } from '../../features/shared/components/ui/switch';
/**
* EN: User Profile Page
* VI: Trang Profile của User
*
* Features:
* - View and edit personal information
* - Change password
* - Account settings
* - Preferences management
*/
export default function ProfilePage() {
const { user, isAuthenticated, updateProfile } = useAuthStore();
const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences'>('profile');
const [isEditing, setIsEditing] = useState(false);
// Mock preferences (in real app, this would come from a preferences store)
const [preferences, setPreferences] = useState({
emailNotifications: true,
pushNotifications: false,
darkMode: false,
language: 'en',
});
if (!isAuthenticated || !user) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Please sign in</h2>
<p className="text-gray-600">
You need to be signed in to access your profile.
</p>
</Card>
</div>
);
}
const handleProfileUpdate = async (data: any) => {
try {
// EN: In a real app, this would call an API, then update the store
// VI: Trong app thực tế, phần này sẽ gọi API, sau đó cập nhật store
console.log('Updating profile:', data);
updateProfile(data);
setIsEditing(false);
} catch (error) {
console.error('Failed to update profile:', error);
}
};
const handlePreferenceChange = (key: string, value: any) => {
setPreferences(prev => ({ ...prev, [key]: value }));
};
const tabs = [
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'security', label: 'Security', icon: Lock },
{ id: 'preferences', label: 'Preferences', icon: Bell },
];
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">My Profile</h1>
<p className="text-gray-600">Manage your account settings and preferences</p>
</div>
{/* Profile Overview Card */}
<Card className="p-6 mb-8">
<div className="flex items-center space-x-6">
<div className="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-bold text-2xl">
{user.email[0].toUpperCase()}
</span>
</div>
<div className="flex-1">
<h2 className="text-xl font-semibold text-gray-900">{user.email}</h2>
<p className="text-gray-600">Member since {new Date(user.createdAt).getFullYear()}</p>
<div className="flex items-center mt-2">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${user.role === 'SUPER_ADMIN' ? 'bg-red-100 text-red-800' :
user.role === 'ADMIN' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{user.role}
</span>
<span className={`ml-3 inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{user.isActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
</div>
</Card>
{/* Tabs */}
<div className="flex space-x-1 mb-6 bg-white p-1 rounded-lg border">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-medium transition-all ${activeTab === tab.id
? 'bg-blue-100 text-blue-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Tab Content */}
<div className="space-y-6">
{/* Profile Tab */}
{activeTab === 'profile' && (
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900">Personal Information</h3>
<p className="text-gray-600">Update your personal details and contact information</p>
</div>
{!isEditing && (
<Button onClick={() => setIsEditing(true)}>
Edit Profile
</Button>
)}
</div>
{isEditing ? (
<UserForm
user={user}
onSubmit={handleProfileUpdate}
onCancel={() => setIsEditing(false)}
/>
) : (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Mail className="w-5 h-5 text-gray-400" />
<span className="text-gray-900">{user.email}</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Role
</label>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<User className="w-5 h-5 text-gray-400" />
<span className="text-gray-900 capitalize">{user.role.toLowerCase()}</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Status
</label>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<span className={`w-3 h-3 rounded-full ${user.isActive ? 'bg-green-500' : 'bg-red-500'
}`} />
<span className="text-gray-900">
{user.isActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Member Since
</label>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<span className="text-gray-900">
{new Date(user.createdAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
</div>
)}
</Card>
)}
{/* Security Tab */}
{activeTab === 'security' && (
<Card className="p-6">
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900">Security Settings</h3>
<p className="text-gray-600">Manage your password and security preferences</p>
</div>
<div className="space-y-6">
<div>
<h4 className="text-md font-medium text-gray-900 mb-4">Change Password</h4>
<div className="space-y-4 max-w-md">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Current Password
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter current password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter new password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm New Password
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Confirm new password"
/>
</div>
<Button>Update Password</Button>
</div>
</div>
</div>
</Card>
)}
{/* Preferences Tab */}
{activeTab === 'preferences' && (
<Card className="p-6">
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900">Preferences</h3>
<p className="text-gray-600">Customize your experience and notification settings</p>
</div>
<div className="space-y-6">
<div>
<h4 className="text-md font-medium text-gray-900 mb-4 flex items-center">
<Bell className="w-5 h-5 mr-2" />
Notifications
</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Email Notifications</p>
<p className="text-sm text-gray-600">Receive notifications via email</p>
</div>
<Switch
checked={preferences.emailNotifications}
onCheckedChange={(checked) => handlePreferenceChange('emailNotifications', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Push Notifications</p>
<p className="text-sm text-gray-600">Receive push notifications in your browser</p>
</div>
<Switch
checked={preferences.pushNotifications}
onCheckedChange={(checked) => handlePreferenceChange('pushNotifications', checked)}
/>
</div>
</div>
</div>
<div>
<h4 className="text-md font-medium text-gray-900 mb-4 flex items-center">
<Palette className="w-5 h-5 mr-2" />
Appearance
</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Dark Mode</p>
<p className="text-sm text-gray-600">Use dark theme for the interface</p>
</div>
<Switch
checked={preferences.darkMode}
onCheckedChange={(checked) => handlePreferenceChange('darkMode', checked)}
/>
</div>
</div>
</div>
<div>
<h4 className="text-md font-medium text-gray-900 mb-4 flex items-center">
<Globe className="w-5 h-5 mr-2" />
Language & Region
</h4>
<div className="max-w-xs">
<label className="block text-sm font-medium text-gray-700 mb-2">
Language
</label>
<select
value={preferences.language}
onChange={(e) => handlePreferenceChange('language', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="en">English</option>
<option value="vi">Tiếng Việt</option>
</select>
</div>
</div>
</div>
</Card>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,315 +0,0 @@
'use client';
import React, { useState } from 'react';
import {
Settings,
Bell,
Shield,
Palette,
Globe,
User,
Moon,
Sun,
Monitor
} from 'lucide-react';
import { useAuthStore } from '../../stores/auth-store';
import { Button } from '../../features/shared/components/ui/button';
import { Card } from '../../features/shared/components/ui/card';
import { Switch } from '../../features/shared/components/ui/switch';
/**
* EN: Settings Page
* VI: Trang Settings
*
* Features:
* - Theme preferences (light/dark/auto)
* - Language settings
* - Notification preferences
* - Privacy settings
* - Account management
*/
export default function SettingsPage() {
const { user, isAuthenticated, logout } = useAuthStore();
const [settings, setSettings] = useState({
theme: 'system', // 'light', 'dark', 'system'
language: 'en',
emailNotifications: true,
pushNotifications: false,
marketingEmails: false,
profileVisibility: 'private', // 'public', 'private'
dataSharing: false,
});
if (!isAuthenticated || !user) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="p-8 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Please sign in</h2>
<p className="text-gray-600">
You need to be signed in to access settings.
</p>
</Card>
</div>
);
}
const handleSettingChange = (key: string, value: any) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
const handleSaveSettings = () => {
// In a real app, this would save to backend
console.log('Saving settings:', settings);
// Show success message
};
const handleExportData = () => {
// Mock data export
const data = {
user: user,
settings: settings,
exportDate: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `user-data-${user.id}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleDeleteAccount = () => {
if (confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
// In a real app, this would call delete API
console.log('Deleting account...');
logout();
}
};
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Settings</h1>
<p className="text-gray-600">Manage your account preferences and privacy settings</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Settings */}
<div className="lg:col-span-2 space-y-6">
{/* Appearance */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-6">
<Palette className="w-6 h-6 text-blue-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Appearance</h3>
<p className="text-gray-600">Customize how the app looks and feels</p>
</div>
</div>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Theme
</label>
<div className="grid grid-cols-3 gap-3">
{[
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Monitor },
].map((theme) => {
const Icon = theme.icon;
return (
<button
key={theme.value}
onClick={() => handleSettingChange('theme', theme.value)}
className={`p-4 border rounded-lg text-center transition-all ${
settings.theme === theme.value
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<Icon className="w-6 h-6 mx-auto mb-2" />
<span className="text-sm font-medium">{theme.label}</span>
</button>
);
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Language
</label>
<select
value={settings.language}
onChange={(e) => handleSettingChange('language', e.target.value)}
className="w-full max-w-xs px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="en">English</option>
<option value="vi">Tiếng Việt</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
</div>
</Card>
{/* Notifications */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-6">
<Bell className="w-6 h-6 text-green-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
<p className="text-gray-600">Choose what notifications you want to receive</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Email Notifications</p>
<p className="text-sm text-gray-600">Receive important updates via email</p>
</div>
<Switch
checked={settings.emailNotifications}
onCheckedChange={(checked) => handleSettingChange('emailNotifications', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Push Notifications</p>
<p className="text-sm text-gray-600">Receive notifications in your browser</p>
</div>
<Switch
checked={settings.pushNotifications}
onCheckedChange={(checked) => handleSettingChange('pushNotifications', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Marketing Emails</p>
<p className="text-sm text-gray-600">Receive promotional content and updates</p>
</div>
<Switch
checked={settings.marketingEmails}
onCheckedChange={(checked) => handleSettingChange('marketingEmails', checked)}
/>
</div>
</div>
</Card>
{/* Privacy */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-6">
<Shield className="w-6 h-6 text-purple-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Privacy</h3>
<p className="text-gray-600">Control your privacy and data sharing preferences</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Profile Visibility
</label>
<select
value={settings.profileVisibility}
onChange={(e) => handleSettingChange('profileVisibility', e.target.value)}
className="w-full max-w-xs px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="public">Public - Anyone can see my profile</option>
<option value="private">Private - Only I can see my profile</option>
</select>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Data Sharing</p>
<p className="text-sm text-gray-600">Allow anonymous usage data to help improve the service</p>
</div>
<Switch
checked={settings.dataSharing}
onCheckedChange={(checked) => handleSettingChange('dataSharing', checked)}
/>
</div>
</div>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Account Info */}
<Card className="p-6">
<div className="flex items-center space-x-3 mb-4">
<User className="w-6 h-6 text-gray-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Account</h3>
</div>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Email</p>
<p className="font-medium text-gray-900">{user.email}</p>
</div>
<div>
<p className="text-sm text-gray-600">Role</p>
<p className="font-medium text-gray-900 capitalize">{user.role.toLowerCase()}</p>
</div>
<div>
<p className="text-sm text-gray-600">Status</p>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{user.isActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
</Card>
{/* Actions */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Account Actions</h3>
<div className="space-y-3">
<Button onClick={handleSaveSettings} className="w-full">
Save Settings
</Button>
<Button variant="secondary" onClick={handleExportData} className="w-full">
Export My Data
</Button>
<Button variant="secondary" onClick={() => window.location.href = '/profile'} className="w-full">
Edit Profile
</Button>
</div>
</Card>
{/* Danger Zone */}
<Card className="p-6 border-red-200 bg-red-50">
<h3 className="text-lg font-semibold text-red-900 mb-4">Danger Zone</h3>
<div className="space-y-3">
<Button
variant="secondary"
onClick={handleDeleteAccount}
className="w-full border-red-300 text-red-700 hover:bg-red-50"
>
Delete Account
</Button>
<p className="text-xs text-red-600">
This action cannot be undone. All your data will be permanently deleted.
</p>
</div>
</Card>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,613 +0,0 @@
# Design System Documentation
> **Hệ thống thiết kế toàn diện cho GoodGo Web Client**
>
> Tài liệu này mô tả đầy đủ các design deliverables chuyên nghiệp, từ UX research đến UI implementation, giúp team phát triển và thiết kế làm việc hiệu quả.
## 📚 Mục lục
- [1. UX Foundation - Nền tảng trải nghiệm người dùng](#1-ux-foundation)
- [2. UI Visual Design - Thiết kế giao diện](#2-ui-visual-design)
- [3. Design System - Hệ thống thiết kế](#3-design-system)
- [4. Component Library - Thư viện thành phần](#4-component-library)
- [5. Developer Handoff - Bàn giao cho Dev](#5-developer-handoff)
---
## 1. UX Foundation - Nền tảng trải nghiệm người dùng
### 1.1 Information Architecture
#### Sitemap
**Vị trí**: `src/docs/ux/sitemap.md`
Cấu trúc tổng quan của ứng dụng:
```
GoodGo Web Client
├── Authentication
│ ├── Login (/login)
│ ├── Register (/register)
│ └── Forgot Password (/forgot-password)
├── Dashboard (/)
│ ├── Overview
│ ├── Analytics
│ └── Quick Actions
├── Profile (/profile)
│ ├── Settings
│ └── Preferences
└── [Add more sections...]
```
**Cách sử dụng**:
- Cập nhật file `ux/sitemap.md` khi thêm/bớt trang
- Dùng cho planning và onboarding team mới
- Tham khảo khi thiết kế navigation
---
### 1.2 User Flows
**Vị trí**: `src/docs/ux/flows/`
Các luồng người dùng quan trọng:
#### Authentication Flow
```
[Landing] → [Login Page] → [Email Input] → [Password Input]
[Validation] → [Success: Dashboard]
[Error: Show Message]
Alternative: Forgot Password
[Login Page] → [Forgot Password Link] → [Email Input]
→ [Send Reset Email] → [Success Message]
```
**Files**:
- `flows/auth-login.md` - Luồng đăng nhập
- `flows/auth-register.md` - Luồng đăng ký
- `flows/auth-password-reset.md` - Luồng reset mật khẩu
**Template cho mỗi flow**:
```markdown
# [Flow Name]
## Mục tiêu
Người dùng cần đạt được điều gì?
## Entry Points
Từ đâu người dùng bắt đầu?
## Steps
1. Step 1: Action → Result
2. Step 2: Action → Result
...
## Success Criteria
Làm thế nào để biết flow thành công?
## Error States
Các lỗi có thể xảy ra và cách xử lý?
## Figma Link
[Link to prototype]
```
---
### 1.3 Wireframes
**Vị trí**: `src/docs/ux/wireframes/`
#### Low-Fidelity (Concept)
- Sketch thủ công hoặc basic blocks
- Focus vào layout và content hierarchy
- Không có color, chỉ grayscale
#### Mid-Fidelity (Structure)
- Chi tiết hơn về components
- Content placeholders rõ ràng
- Grid system được áp dụng
- Chưa có branding/style
**Files**:
- `wireframes/auth-pages-low.md` - Low-fi wireframes
- `wireframes/auth-pages-mid.md` - Mid-fi wireframes
**Lưu wireframes**:
- Export từ Figma/Sketch dạng PNG
- Đặt trong `public/design/wireframes/`
- Link trong markdown files
---
## 2. UI Visual Design - Thiết kế giao diện
### 2.1 Moodboard
**Vị trí**: `src/docs/ui/moodboard.md`
Định hướng phong cách visual cho project:
#### X.ai Minimal Style (Current Direction)
- **Keywords**: Clean, Minimal, Sophisticated, Tech-forward
- **Inspiration**: X.ai, Linear, Vercel
- **Color Direction**: Dark backgrounds, vibrant accents
- **Typography**: Geometric sans-serif (Inter, Geist)
- **Visual Treatment**: Subtle glassmorphism, minimal shadows
**References**:
- X.ai authentication pages
- Linear app interface
- Vercel dashboard
---
### 2.2 High-Fidelity Mockups
**Vị trí**: `src/docs/ui/mockups/`
Thiết kế hoàn chỉnh của tất cả screens:
#### States Required for Each Screen
-**Default State** - Trạng thái bình thường
-**Hover State** - Khi di chuột qua interactive elements
-**Active State** - Khi click/focus
-**Error State** - Khi có lỗi validation
-**Success State** - Khi thao tác thành công
-**Loading State** - Khi đang xử lý
-**Empty State** - Khi không có data
-**Disabled State** - Khi element bị vô hiệu hóa
**Files**:
- `mockups/auth-login-states.md` - Tất cả states của login page
- `mockups/auth-register-states.md` - Tất cả states của register page
- `mockups/auth-forgot-password-states.md` - States của forgot password
---
### 2.3 Responsive Design
**Breakpoints** (Tailwind CSS):
- Mobile: `< 640px` (sm)
- Tablet: `640px - 1024px` (md, lg)
- Desktop: `> 1024px` (xl, 2xl)
**Files**:
- `ui/responsive/mobile-specs.md`
- `ui/responsive/tablet-specs.md`
- `ui/responsive/desktop-specs.md`
**Quy tắc responsive**:
- Mobile-first approach
- Stack elements vertically on mobile
- 2-column layout on tablet
- 3+ column layout on desktop
- Touch targets minimum 44x44px on mobile
---
## 3. Design System - Hệ thống thiết kế
### 3.1 Color Palette
**Vị trí**: `src/styles/theme.css` (Source of Truth)
#### X.ai Minimal Color Palette
**Dark Theme (Default)**:
```css
/* Backgrounds */
--bg-primary: #15202b; /* Warm dark gray (not pure black) */
--bg-secondary: #1a2734; /* Lighter variant */
--bg-tertiary: #1f2f3d; /* Even lighter */
--bg-elevated: #243442; /* Elevated surfaces */
/* Brand/Accent */
--accent-primary: #1D9BF0; /* X.ai blue */
--accent-primary-hover: #1a8cd8;
--accent-secondary: #657786;
/* Text */
--text-primary: #FFFFFF; /* Pure white */
--text-secondary: #8899A6; /* Light gray */
--text-tertiary: #657786; /* Mid gray */
/* Status Colors */
--accent-success: #10B981; /* Green */
--accent-error: #EF4444; /* Red */
--accent-warning: #F59E0B; /* Amber */
--accent-info: #1D9BF0; /* Blue */
/* Borders */
--border-primary: #38444d;
--border-secondary: #4a5966;
--border-focus: #1D9BF0; /* X.ai blue */
```
**Light Theme**:
```css
--bg-primary: #FFFFFF;
--accent-primary: #1D9BF0;
--text-primary: #1D1D1F;
```
**Color Usage Guidelines**:
- Backgrounds: Use `--bg-*` variables
- Interactive elements: Use `--accent-primary` for primary actions
- Text: Use `--text-*` with proper hierarchy
- Never use hardcoded hex values in components
**Accessibility**:
- All color combinations meet WCAG 2.1 AA (4.5:1 contrast ratio)
- Test with WebAIM Contrast Checker
---
### 3.2 Typography
**Font Family**:
```css
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
```
**Type Scale**:
```css
/* Headings */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
--text-5xl: 3rem; /* 48px */
/* Line Heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Font Weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--font-extrabold: 800;
```
**Typography Usage**:
- H1: `text-4xl font-extrabold` - Page titles
- H2: `text-3xl font-bold` - Section headers
- H3: `text-2xl font-semibold` - Subsection headers
- Body: `text-base font-normal` - Default text
- Small: `text-sm` - Secondary info
- Caption: `text-xs` - Captions, labels
---
### 3.3 Spacing System
**8-Point Grid System**:
```css
--spacing-0: 0;
--spacing-1: 0.25rem; /* 4px */
--spacing-2: 0.5rem; /* 8px */
--spacing-3: 0.75rem; /* 12px */
--spacing-4: 1rem; /* 16px */
--spacing-5: 1.25rem; /* 20px */
--spacing-6: 1.5rem; /* 24px */
--spacing-8: 2rem; /* 32px */
--spacing-10: 2.5rem; /* 40px */
--spacing-12: 3rem; /* 48px */
--spacing-16: 4rem; /* 64px */
--spacing-20: 5rem; /* 80px */
```
**Spacing Guidelines**:
- Form fields: `space-y-4` (16px)
- Card padding: `p-8` (32px)
- Section spacing: `space-y-12` (48px)
- Container max-width: `max-w-md` (448px) for auth forms
---
### 3.4 Border Radius
```css
--radius-sm: 0.375rem; /* 6px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 0.75rem; /* 12px */
--radius-xl: 1rem; /* 16px */
--radius-2xl: 1.5rem; /* 24px */
--radius-full: 9999px; /* Fully rounded */
```
---
### 3.5 Shadows
```css
/* Subtle shadows for minimal design */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2);
/* Glass shadows */
--shadow-glass-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-glass-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
```
---
### 3.6 Animations & Transitions
**Duration**:
```css
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
```
**Easing**:
```css
--ease-snap: cubic-bezier(0.4, 0.0, 0.2, 1);
--ease-smooth: cubic-bezier(0.4, 0.0, 0.6, 1);
```
**Common Animations**:
- Hover scale: `hover:scale-[1.02] transition-transform duration-150`
- Fade in: `animate-in fade-in duration-300`
- Float: `animate-float` (custom keyframe in theme.css)
---
## 4. Component Library - Thư viện thành phần
### 4.1 Components Catalog
**Vị trí**: Storybook - `npm run storybook`
Tất cả components đã được document trong Storybook với:
- Visual examples
- Props API
- Usage guidelines
- Accessibility notes
- Code snippets
### 4.2 Core Components
#### Button
**Path**: `src/features/shared/components/ui/button/`
**Variants**:
- `brand` - Primary CTA (X.ai blue)
- `secondary` - Secondary actions
- `ghost` - Tertiary actions
- `danger` - Destructive actions
**Sizes**: `sm`, `md`, `lg`
**States**: default, hover, active, loading, disabled
**Usage**:
```tsx
<Button variant="brand" size="lg">
Sign In
</Button>
```
---
#### Input
**Path**: `src/features/shared/components/ui/input/`
**Features**:
- Label support
- Error messages
- Description text
- Password visibility toggle
- Focus states with X.ai blue
**States**: default, focus, error, disabled
**Usage**:
```tsx
<Input
label="Email"
type="email"
placeholder="you@example.com"
error="Invalid email"
/>
```
---
#### Card
**Path**: `src/features/shared/components/ui/card/`
**Features**:
- Glass effect variants
- Hover states
- Border options
**Usage**:
```tsx
<Card className="glass-card p-8">
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
Content here
</CardContent>
</Card>
```
---
### 4.3 Glass Effects
**Path**: `src/styles/glass.css`
**Classes**:
- `.glass-card` - Card containers
- `.glass-input` - Input fields
- `.glass-bg` - Generic glass background
**X.ai Minimal Glass**:
- Ultra-subtle transparency (1-5%)
- Thin borders (3-10% white)
- Minimal blur (4-12px)
- Works on #15202b background
---
## 5. Developer Handoff - Bàn giao cho Dev
### 5.1 Design Specs
**Tools**:
- Figma Dev Mode (recommended)
- Zeplin
- Measure Chrome Extension
**Specs to Include**:
- Exact pixel measurements
- Spacing between elements
- Font sizes and weights
- Color values (reference CSS variables)
- Border radius
- Shadow values
---
### 5.2 Assets Export
**Location**: `public/design/assets/`
**Format Guidelines**:
- **Icons**: SVG (preferred), PNG @2x, @3x
- **Images**: WebP (preferred), PNG, JPEG
- **Illustrations**: SVG for vector graphics
- **Logo**: SVG + PNG variants
**Export Checklist**:
- [ ] Optimize SVGs (remove unnecessary metadata)
- [ ] Export @1x, @2x, @3x for raster images
- [ ] Name files descriptively: `icon-close.svg`, `logo-light.svg`
- [ ] Organize in folders by category
---
### 5.3 Implementation Notes
**Vị trí**: `src/docs/implementation/`
**Files**:
- `implementation/auth-pages-notes.md` - Ghi chú cho auth pages
- `implementation/components-notes.md` - Component implementation details
**Template**:
```markdown
# [Component/Page Name] Implementation Notes
## Design Intent
Mục đích thiết kế là gì?
## Technical Requirements
- Framework: Next.js 14
- Styling: Tailwind CSS
- Animations: Framer Motion (if needed)
## Accessibility Requirements
- ARIA attributes needed
- Keyboard navigation
- Screen reader considerations
## Edge Cases
- What happens when...?
- How to handle...?
## Dependencies
- Required components
- Third-party libraries
## Testing Checklist
- [ ] Visual regression
- [ ] Accessibility (a11y)
- [ ] Responsive on all breakpoints
- [ ] Cross-browser compatibility
```
---
### 5.4 Component API Documentation
Mỗi component cần có:
**Props Table**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'brand' \| 'secondary'` | `'brand'` | Visual style variant |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the button |
| `disabled` | `boolean` | `false` | Disable interaction |
**Example Usage**:
```tsx
// Basic usage
<Button>Click me</Button>
// With all props
<Button
variant="brand"
size="lg"
disabled={false}
onClick={handleClick}
>
Sign In
</Button>
```
---
## 📝 Maintenance & Updates
### When to Update This Documentation
1. **New Feature Added** → Update sitemap, user flows, components
2. **Visual Redesign** → Update moodboard, mockups, color palette
3. **Component Changes** → Update component specs in Storybook
4. **New Page** → Add to sitemap, create wireframes/mockups
5. **Accessibility Improvements** → Update WCAG compliance docs
### Version Control
- Use semantic versioning for design system updates
- Document breaking changes
- Keep changelog in `DESIGN_SYSTEM_CHANGELOG.md`
---
## 🔗 Quick Links
- [Storybook](http://localhost:6006) - Component library
- [WCAG Compliance](./WCAG_COMPLIANCE.md) - Accessibility guidelines
- [Performance](./PERFORMANCE.md) - Performance best practices
- [Figma Design Files](#) - (Add your Figma link here)
---
## 📞 Contact
**Design Team**: design@goodgo.com
**Dev Team**: dev@goodgo.com
---
**Last Updated**: 2026-01-04
**Version**: 2.0.0 (X.ai Minimal Redesign)

View File

@@ -1,504 +0,0 @@
# ✅ Documentation Complete - Summary
> **Professional UX/UI Design Deliverables**
>
> Hệ thống documentation hoàn chỉnh cho GoodGo Web Client
**Created**: 2026-01-04
**Total Files**: 12 markdown documents
**Total Size**: ~106 KB
**Status**: ✅ Ready for Use
---
## 🎯 Tổng quan
Bạn đã yêu cầu tạo **bộ hồ sơ thiết kế UX/UI chuyên nghiệp** theo chuẩn industry standard, không chỉ là "hình ảnh đẹp" mà là tài liệu toàn diện sẵn sàng cho team sử dụng.
**✅ ĐÃ HOÀN THÀNH TẤT CẢ!**
---
## 📊 Documentation Coverage
### ✅ 1. UX Foundation - Nền tảng trải nghiệm
| Document | Size | Status | Description |
|----------|------|--------|-------------|
| [ux/sitemap.md](./ux/sitemap.md) | 2.7K | ✅ | Information architecture, cấu trúc app |
| [ux/flows/auth-login.md](./ux/flows/auth-login.md) | 9.2K | ✅ | Chi tiết login user flow với 8 steps |
| [ux/wireframes/auth-pages-wireframes.md](./ux/wireframes/auth-pages-wireframes.md) | 23K | ✅ | Lo-Fi & Mid-Fi wireframes cho 3 auth pages |
**Coverage**: 3/3 files ✅
**Bao gồm**:
- ✅ Sitemap với navigation patterns
- ✅ Login user flow (entry/exit points, success/error paths)
- ✅ Wireframes (low-fi + mid-fi) cho login, register, forgot password
- ✅ Grid system & spacing specifications
- ✅ Responsive wireframe variants
---
### ✅ 2. UI Visual Design - Thiết kế giao diện
| Document | Size | Status | Description |
|----------|------|--------|-------------|
| [ui/mockups/auth-login-states.md](./ui/mockups/auth-login-states.md) | 9.9K | ✅ | 8 states của login page (default, hover, focus, error, loading, etc.) |
| [ui/responsive/mobile-specs.md](./ui/responsive/mobile-specs.md) | 9.6K | ✅ | Mobile responsive specs (< 640px) |
**Coverage**: 2 files core ✅
**Bao gồm**:
- ✅ All UI states documented (default, hover, focus, active, error, loading, success, disabled)
- ✅ High-fidelity mockup specs với measurements chính xác
- ✅ Mobile-first responsive specifications
- ✅ Typography scale, color palette, spacing
- ✅ Touch targets, virtual keyboard handling
- ✅ Performance targets cho mobile
**TODO (có thể tạo sau)**:
- [ ] Register & Forgot Password mockups (dùng template tương tự)
- [ ] Tablet specs (640-1024px)
- [ ] Desktop specs (> 1024px)
---
### ✅ 3. Design System - Hệ thống thiết kế
| Document | Size | Status | Description |
|----------|------|--------|-------------|
| [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) | 14K | ✅ | Complete design system documentation |
**Coverage**: 100% ✅
**Bao gồm**:
-**UX Foundation**: Sitemap structure, User flows framework, Wireframes guidelines
-**UI Visual Design**: Moodboard guidelines, Mockups structure, Responsive specs
-**Design System**:
- Color palette (X.ai blue #1D9BF0, dark #15202b)
- Typography (Inter, geometric sans-serif)
- Spacing (8-point grid)
- Shadows, Border radius, Animations
-**Component Library**: Button, Input, Card specs
-**Developer Handoff**: Specs, assets export, implementation notes
**X.ai Minimal Design**:
- ✅ Warm dark gray background (#15202b thay vì pure black)
- ✅ X.ai blue accent (#1D9BF0)
- ✅ Removed cosmic background effects
- ✅ Neo-minimalism 2026 style
---
### ✅ 4. Component Library - Thư viện thành phần
**Integration**: ✅ Storybook (đã có sẵn)
**Components Documented**:
- ✅ Button (brand variant with X.ai blue)
- ✅ Input (with focus states, password visibility)
- ✅ Card (glass effects)
**Component Specs Template**: ✅
- [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md) (7.5K)
- Dùng để document thêm components sau
**TODO** (dùng template):
- [ ] Modal spec
- [ ] Dropdown spec
- [ ] Tooltip spec
- [ ] Toast/Alert spec
---
### ✅ 5. Developer Handoff - Bàn giao cho Dev
| Document | Size | Status | Description |
|----------|------|--------|-------------|
| [implementation/auth-pages-implementation.md](./implementation/auth-pages-implementation.md) | 13K | ✅ | Chi tiết implementation guide cho auth redesign |
**Coverage**: 100% ✅
**Bao gồm**:
- ✅ Design intent & rationale
- ✅ Step-by-step technical implementation (7 files to modify)
- ✅ Code examples chi tiết với line numbers
- ✅ Testing requirements (Visual, A11y, Responsive, Functional, Cross-browser)
- ✅ Design specifications (measurements, colors, animations)
- ✅ Common issues & solutions
- ✅ Migration checklist (5 phases)
- ✅ Assets checklist
**Ready for Implementation**: ✅ Developers có thể bắt đầu ngay!
---
### ✅ 6. Templates - Reusable Documentation
| Template | Size | Purpose |
|----------|------|---------|
| [TEMPLATE_USER_FLOW.md](./TEMPLATE_USER_FLOW.md) | 3.9K | Tạo user flows cho features mới |
| [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md) | 7.5K | Document components mới |
**Coverage**: 2 core templates ✅
**Cách dùng**:
```bash
# Copy template
cp src/docs/TEMPLATE_USER_FLOW.md src/docs/ux/flows/checkout-flow.md
# Edit file
# Điền các section theo hướng dẫn
# Link vào README.md
```
---
### ✅ 7. Supporting Documentation
| Document | Size | Status | Description |
|----------|------|--------|-------------|
| [README.md](./README.md) | 9.6K | ✅ | Central hub, navigation, how-to guides |
| [WCAG_COMPLIANCE.md](./WCAG_COMPLIANCE.md) | 1.7K | ✅ | Accessibility guidelines |
| [PERFORMANCE.md](./PERFORMANCE.md) | 2.5K | ✅ | Performance best practices |
**Coverage**: 100% ✅
---
## 📁 File Structure (Complete)
```
src/docs/
├── 📖 README.md (9.6K) ─────────────────── START HERE!
├── 🎨 DESIGN_SYSTEM.md (14K) ────────────── Complete design system
├── ♿ WCAG_COMPLIANCE.md (1.7K) ──────────── Accessibility
├── ⚡ PERFORMANCE.md (2.5K) ──────────────── Performance
├── ✅ DOCUMENTATION_COMPLETE.md ──────────── This file
├── 📝 Templates (Reusable)
│ ├── TEMPLATE_USER_FLOW.md (3.9K) ────── Copy cho user flows mới
│ └── TEMPLATE_COMPONENT_SPEC.md (7.5K) ── Copy cho component specs
├── ux/ ──────────────────────────────────── UX Deliverables
│ ├── sitemap.md (2.7K) ───────────────── ✅ App structure
│ ├── flows/
│ │ └── auth-login.md (9.2K) ────────── ✅ Login flow complete
│ └── wireframes/
│ └── auth-pages-wireframes.md (23K) ─ ✅ Lo-Fi + Mid-Fi
├── ui/ ──────────────────────────────────── UI Design Specs
│ ├── mockups/
│ │ └── auth-login-states.md (9.9K) ─── ✅ 8 UI states
│ └── responsive/
│ └── mobile-specs.md (9.6K) ──────── ✅ Mobile responsive
└── implementation/ ───────────────────────── Developer Handoff
└── auth-pages-implementation.md (13K) ── ✅ Implementation guide
public/design/ ────────────────────────────── Design Assets (folders created)
├── assets/ ──────────────────────────────── Icons, images
└── wireframes/ ──────────────────────────── Exported wireframes
```
**Total**: 12 documentation files, ~106 KB
---
## 🎯 What You Have Now
### ✅ Professional vs Amateur
#### ❌ Design Nghiệp Dư
- Vài file PNG/JPG của UI
- Không có user flows
- Không có design system
- Dev phải "đoán" implementation
- Không có wireframes
- Không có testing guidelines
#### ✅ Design Chuyên Nghiệp (BẠN ĐÃ CÓ!)
**1. UX Foundation**
- ✅ Sitemap (information architecture)
- ✅ User flows (chi tiết từng bước)
- ✅ Wireframes (lo-fi + mid-fi)
**2. UI Visual Design**
- ✅ Moodboard guidelines (X.ai minimal)
- ✅ High-fidelity mockups (8 states)
- ✅ Responsive specifications (mobile)
**3. Design System**
- ✅ Color palette (X.ai blue, warm dark gray)
- ✅ Typography (Inter, scales)
- ✅ Spacing (8-point grid)
- ✅ Components (Button, Input, Card)
- ✅ Shadows, Borders, Animations
**4. Component Library**
- ✅ Storybook integration
- ✅ Component specs template
**5. Developer Handoff**
- ✅ Implementation guide (step-by-step)
- ✅ Code examples (exact line numbers)
- ✅ Testing checklists
- ✅ Assets export guidelines
**6. Reusable Templates**
- ✅ User flow template
- ✅ Component spec template
---
## 🚀 How to Use
### For Designers
1. **Start Here**: [README.md](./README.md)
2. **Understand System**: [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md)
3. **See Examples**:
- [Login Flow](./ux/flows/auth-login.md)
- [Login Mockups](./ui/mockups/auth-login-states.md)
- [Wireframes](./ux/wireframes/auth-pages-wireframes.md)
4. **Create New**:
- Copy [TEMPLATE_USER_FLOW.md](./TEMPLATE_USER_FLOW.md)
- Copy [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md)
---
### For Developers
1. **Implementation Guide**: [auth-pages-implementation.md](./implementation/auth-pages-implementation.md)
2. **Design Tokens**: `src/styles/theme.css`
3. **Component Library**: `npm run storybook`
4. **Follow Steps**:
```bash
# Step 1: Update theme.css
# Step 2: Update glass.css
# Step 3: Update auth pages
# Step 4: Update components
# Step 5: Test (4 types)
```
---
### For Product Managers
1. **See Structure**: [Sitemap](./ux/sitemap.md)
2. **Understand Flows**: [Login Flow](./ux/flows/auth-login.md)
3. **Review Designs**: [Mockups](./ui/mockups/auth-login-states.md)
4. **Plan Features**: Use templates for new flows
---
### For QA/Testing
1. **Test Checklists**: In [implementation guide](./implementation/auth-pages-implementation.md)
2. **Accessibility**: [WCAG_COMPLIANCE.md](./WCAG_COMPLIANCE.md)
3. **Responsive**: [Mobile Specs](./ui/responsive/mobile-specs.md)
4. **User Flows**: [Login Flow](./ux/flows/auth-login.md)
---
## 📊 Documentation Metrics
### Completion Status
**Core Documentation**: ✅ 100%
- Design System: ✅
- Implementation Guide: ✅
- User Flows: ✅ (1/3 - login done, template available)
- Wireframes: ✅
- Mockups: ✅ (1/3 - login done, template available)
- Responsive Specs: ✅ (1/3 - mobile done)
- Templates: ✅
**Optional Enhancements**: 📋 Available via Templates
- Register flow: Use TEMPLATE_USER_FLOW.md
- Forgot password flow: Use TEMPLATE_USER_FLOW.md
- Register mockups: Use auth-login-states.md as reference
- Tablet specs: Use mobile-specs.md as reference
- Desktop specs: Use mobile-specs.md as reference
- More component specs: Use TEMPLATE_COMPONENT_SPEC.md
### Files Created
| Category | Files | Size | Status |
|----------|-------|------|--------|
| **Core Docs** | 4 | 28K | ✅ Complete |
| **UX** | 3 | 35K | ✅ Complete |
| **UI** | 2 | 19.5K | ✅ Complete |
| **Implementation** | 1 | 13K | ✅ Complete |
| **Templates** | 2 | 11.4K | ✅ Complete |
| **Total** | **12** | **~106K** | **✅ Complete** |
---
## 🎨 X.ai Minimal Redesign - Ready to Implement
**Design Approved**: ✅
**Documentation Complete**: ✅
**Implementation Guide**: ✅
**Testing Checklist**: ✅
**Developers can start now!**
### Files to Modify (from implementation guide)
1. ✅ `src/styles/theme.css` - Color palette
2. ✅ `src/styles/glass.css` - Glass effects
3. ✅ `src/app/(auth)/login/page.tsx` - Login page
4. ✅ `src/app/(auth)/register/page.tsx` - Register page
5. ✅ `src/app/(auth)/forgot-password/page.tsx` - Forgot password
6. ✅ `src/features/shared/components/ui/input/input.tsx` - Input component
7. ✅ `src/features/shared/components/ui/button/button.tsx` - Button component
**All specs documented in**: [auth-pages-implementation.md](./implementation/auth-pages-implementation.md)
---
## 💡 Benefits of This Documentation System
### 1. Faster Development ⚡
- Dev có guide chi tiết, không cần đoán
- Code examples với line numbers chính xác
- Step-by-step implementation
### 2. Design Consistency 🎨
- Design system rõ ràng, dễ follow
- Color palette, typography, spacing defined
- Components reusable
### 3. Better Collaboration 🤝
- Docs làm cầu nối Design-Dev-QA-PM
- Mọi người cùng reference một nguồn
- Giảm miscommunication
### 4. Scalability 📈
- Templates sẵn sàng cho feature mới
- Copy & customize, không phải làm lại
- Maintainable long-term
### 5. Accessibility ♿
- Built-in WCAG compliance guidelines
- Color contrast validated
- Keyboard navigation documented
### 6. Professional Quality 🏆
- Industry standard deliverables
- Ready for stakeholder presentation
- Ready for developer handoff
---
## 📋 Next Steps
### Immediate (Now)
**For Developers**:
1. ✅ Read [Implementation Guide](./implementation/auth-pages-implementation.md)
2. ✅ Start implementing (follow 5 steps)
3. ✅ Test according to checklists
**For Designers**:
1. ✅ Review all docs
2. ✅ Create Figma prototypes (link in docs)
3. ✅ Add screenshots to `public/design/`
---
### Short-term (This Week)
**Optional Enhancements**:
- [ ] Create Register & Forgot Password flows (use template)
- [ ] Create Register & Forgot Password mockups (use login as reference)
- [ ] Create Tablet & Desktop responsive specs (use mobile as reference)
- [ ] Add Figma links to all docs
- [ ] Export and add screenshots
---
### Long-term (This Month)
**Scale the System**:
- [ ] Document 10 core components (use template)
- [ ] Create more user flows (checkout, profile, etc.)
- [ ] Setup Chromatic for visual regression
- [ ] Accessibility audit all pages
- [ ] Performance benchmarks
---
## 🔗 Quick Reference
| Need | Go To |
|------|-------|
| **Overview** | [README.md](./README.md) |
| **Design System** | [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) |
| **Implement Auth** | [auth-pages-implementation.md](./implementation/auth-pages-implementation.md) |
| **See User Flow** | [auth-login.md](./ux/flows/auth-login.md) |
| **See Wireframes** | [auth-pages-wireframes.md](./ux/wireframes/auth-pages-wireframes.md) |
| **See Mockups** | [auth-login-states.md](./ui/mockups/auth-login-states.md) |
| **Mobile Specs** | [mobile-specs.md](./ui/responsive/mobile-specs.md) |
| **Create Flow** | [TEMPLATE_USER_FLOW.md](./TEMPLATE_USER_FLOW.md) |
| **Document Component** | [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md) |
---
## 🎉 Congratulations!
Bạn đã có một **hệ thống documentation UX/UI chuyên nghiệp** hoàn chỉnh!
**✅ Đây KHÔNG PHẢI là vài file hình ảnh**
**✅ Đây LÀ bộ hồ sơ thiết kế toàn diện theo chuẩn industry**
### So sánh
**Before (Amateur)**:
- 3 file PNG của UI screens
- Không có context
- Dev không biết làm thế nào
**After (Professional - BẠN ĐÃ CÓ!)**:
- 12 markdown documents
- ~106 KB documentation
- UX Foundation (sitemap, flows, wireframes)
- UI Design (mockups, responsive specs)
- Design System (colors, typography, spacing, components)
- Developer Handoff (implementation guide, testing)
- Reusable Templates (scale cho tương lai)
---
## 📞 Support
**Questions about**:
- Documentation structure → [README.md](./README.md)
- Design system → [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md)
- Implementation → [auth-pages-implementation.md](./implementation/auth-pages-implementation.md)
**Team Contacts**:
- Design Team: design@goodgo.com
- Dev Team: dev@goodgo.com
- Product Team: product@goodgo.com
---
**Created**: 2026-01-04
**Last Updated**: 2026-01-04
**Version**: 1.0.0
**Status**: ✅ Complete & Ready for Use
---
## 🚀 Ready to Build!
Documentation: ✅
Design: ✅
Specs: ✅
Testing: ✅
**Let's ship it! 🎉**

View File

@@ -1,77 +0,0 @@
# Performance Optimization Documentation
## Code Splitting
### Route-based Splitting
- Chat page: Lazy loaded TypingIndicator
- Admin dashboard: Lazy loaded chart components (UserGrowthChart, RevenueChart)
- Admin settings: Lazy loaded settings forms (GeneralSettings, EmailSettings, SecuritySettings)
- Admin users: Lazy loaded UserDetailsModal
### Component-based Splitting
- Heavy components wrapped with `React.lazy()` and `React.Suspense`
- Fallback loading states provided for better UX
## Image Optimization
### Next.js Image Configuration
- **Formats**: AVIF and WebP (automatic format selection)
- **Device sizes**: Responsive breakpoints (640px to 3840px)
- **Image sizes**: Optimized sizes for different use cases
- **Lazy loading**: All images use `loading="lazy"` attribute
### Implementation
- Avatar images: Using Radix UI AvatarImage (supports lazy loading)
- QR codes: Using `<img>` with `loading="lazy"` (data URLs not supported by Next.js Image)
## Lazy Loading
### Components
- TypingIndicator: Lazy loaded in chat page
- Chart components: Lazy loaded in admin dashboard
- Modal components: Lazy loaded when needed
- Settings forms: Lazy loaded per tab
### Images
- All images use `loading="lazy"` attribute
- Avatar images support lazy loading through Radix UI
### Routes
- Dynamic imports for route-based code splitting (Next.js App Router handles this automatically)
## Caching Strategy
### Browser Caching
- **Static assets**: 1 year cache (`Cache-Control: public, max-age=31536000, immutable`)
- Configured in `next.config.js` headers
### API Response Caching
- **React Query**: 5 minutes stale time, 10 minutes cache time
- Automatic refetching on window focus (disabled for better UX)
- Retry failed requests once
### Session Storage
- User data: Stored in Zustand stores with persistence
- Chat conversations: Persisted to localStorage
- Theme preferences: Persisted to localStorage
## Performance Targets
### Lighthouse Scores
- **Performance**: 90+
- **Accessibility**: 95+
- **Best Practices**: 95+
- **SEO**: 100
### Core Web Vitals
- **LCP** (Largest Contentful Paint): < 2.5s
- **FID** (First Input Delay): < 100ms
- **CLS** (Cumulative Layout Shift): < 0.1
## Optimization Techniques
1. **Code Splitting**: Route and component-based splitting
2. **Image Optimization**: WebP/AVIF formats, responsive images, lazy loading
3. **Caching**: Browser caching for static assets, React Query for API responses
4. **Compression**: Gzip/Brotli compression enabled
5. **Console Removal**: Production builds remove console.log (except errors/warnings)

View File

@@ -1,344 +0,0 @@
# Documentation Hub
> **Central documentation for GoodGo Web Client**
>
> Professional design deliverables, technical specs, and guidelines
## 📚 Documentation Structure
```
src/docs/
├── 📖 README.md (this file)
├── 🎨 DESIGN_SYSTEM.md - Complete design system documentation ✅
├── ♿ WCAG_COMPLIANCE.md - Accessibility guidelines ✅
├── ⚡ PERFORMANCE.md - Performance best practices ✅
├── 📝 TEMPLATE_USER_FLOW.md - Template for creating user flows ✅
├── 📝 TEMPLATE_COMPONENT_SPEC.md - Template for component specs ✅
├── ✅ DOCUMENTATION_COMPLETE.md - Summary of all docs ✅
├── ux/ - UX Research & Deliverables
│ ├── sitemap.md - Information architecture ✅
│ ├── flows/
│ │ ├── auth-login.md - Login flow ✅
│ │ ├── auth-register.md - Registration flow (use template)
│ │ └── auth-password-reset.md - Password reset flow (use template)
│ └── wireframes/
│ └── auth-pages-wireframes.md - Lo-Fi + Mid-Fi wireframes ✅
├── ui/ - UI Design Documentation
│ ├── MOODBOARD.md - Visual direction & X.ai design philosophy ✅
│ ├── mockups/
│ │ ├── auth-login-states.md - All 8 states of login page ✅
│ │ ├── auth-register-states.md - (use login as template)
│ │ └── auth-forgot-password-states.md - (use login as template)
│ ├── responsive/
│ │ ├── mobile-specs.md - Mobile breakpoint specs ✅
│ │ ├── tablet-specs.md - (use mobile as template)
│ │ └── desktop-specs.md - (use mobile as template)
│ └── components/ - (use TEMPLATE_COMPONENT_SPEC.md)
└── implementation/
└── auth-pages-implementation.md - Auth pages guide ✅
```
---
## 🎯 Quick Links
### For Designers
- **Start here**: [Design System](./DESIGN_SYSTEM.md)
- **Visual direction**: [Moodboard](./ui/MOODBOARD.md) - X.ai design philosophy
- **Wireframes**: [Auth Wireframes](./ux/wireframes/auth-pages-wireframes.md)
- **Mockups**: [Login States](./ui/mockups/auth-login-states.md)
- **Create new flow**: Use [User Flow Template](./TEMPLATE_USER_FLOW.md)
- **Document component**: Use [Component Spec Template](./TEMPLATE_COMPONENT_SPEC.md)
- **See structure**: [Sitemap](./ux/sitemap.md)
### For Developers
- **Implementation guide**: [Auth Pages Implementation](./implementation/auth-pages-implementation.md)
- **Design tokens**: [Theme CSS](../styles/theme.css)
- **Components**: Run `npm run storybook`
- **Accessibility**: [WCAG Compliance](./WCAG_COMPLIANCE.md)
### For Product Managers
- **User flows**: [ux/flows/](./ux/flows/)
- **Feature specs**: [implementation/](./implementation/)
- **Analytics**: See individual flow documents
### For QA/Testing
- **Test checklists**: See implementation docs
- **Accessibility**: [WCAG Compliance](./WCAG_COMPLIANCE.md)
- **Performance**: [Performance Guide](./PERFORMANCE.md)
---
## 🚀 Current Project: X.ai Minimal Redesign
**Status**: In Progress ⏳
**Objective**: Redesign 3 authentication pages to match X.ai's 2026 minimal aesthetic
### Completed ✅
- [x] Design system documentation structure
- [x] UX sitemap
- [x] Login user flow
- [x] Implementation guide for auth pages
- [x] Templates for reusable documentation
### In Progress 🏗️
- [ ] High-fidelity mockups for all auth states
- [ ] Component specifications
- [ ] Responsive design specifications
### Pending 📋
- [ ] User flows for register & forgot password
- [ ] Wireframes documentation
- [ ] Moodboard creation
- [ ] Visual regression tests
- [ ] Accessibility audit
---
## 📝 How to Use This Documentation
### Creating a New User Flow
1. Copy [TEMPLATE_USER_FLOW.md](./TEMPLATE_USER_FLOW.md)
2. Save to `ux/flows/[feature-name]-flow.md`
3. Fill in all sections
4. Link Figma prototype
5. Add to sitemap if new page
6. Update this README
**Example**:
```bash
cp src/docs/TEMPLATE_USER_FLOW.md src/docs/ux/flows/checkout-flow.md
# Edit checkout-flow.md
# Add link in sitemap.md
# Update README.md
```
---
### Creating a New Component Spec
1. Copy [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md)
2. Save to `ui/components/[component-name]-spec.md`
3. Fill in all sections
4. Add screenshots
5. Link Figma design
6. Create Storybook story
**Example**:
```bash
cp src/docs/TEMPLATE_COMPONENT_SPEC.md src/docs/ui/components/modal-spec.md
# Edit modal-spec.md
# Add screenshots to public/design/
# Create Storybook story
```
---
### Creating Implementation Documentation
1. Create file in `implementation/[feature-name]-implementation.md`
2. Include:
- Design intent
- Technical requirements
- Step-by-step guide
- Code examples
- Testing checklist
3. Link related docs (flows, specs)
4. Add to this README
---
## 🎨 Design System Quick Reference
### Colors
```css
/* X.ai Minimal Palette */
--bg-primary: #15202b; /* Warm dark gray */
--accent-primary: #1D9BF0; /* X.ai blue */
--text-primary: #FFFFFF; /* Pure white */
```
### Typography
```css
--font-sans: 'Inter', sans-serif;
--text-4xl: 2.25rem; /* Headings */
--text-base: 1rem; /* Body */
```
### Spacing (8-point grid)
```css
--spacing-4: 1rem; /* 16px */
--spacing-8: 2rem; /* 32px */
```
### Breakpoints
```css
Mobile: < 640px
Tablet: 640px - 1024px
Desktop: > 1024px
```
---
## ♿ Accessibility Requirements
All components and pages must meet:
- **WCAG 2.1 Level AA** compliance
- **Contrast ratio** ≥ 4.5:1 for text
- **Keyboard navigation** fully supported
- **Screen reader** compatible
- **Focus indicators** clearly visible (X.ai blue ring)
See [WCAG_COMPLIANCE.md](./WCAG_COMPLIANCE.md) for details.
---
## 📊 Performance Standards
- **Page load**: < 1s
- **Time to Interactive**: < 2s
- **Lighthouse Performance**: ≥ 90
- **Lighthouse Accessibility**: ≥ 95
See [PERFORMANCE.md](./PERFORMANCE.md) for optimization guide.
---
## 🔄 Documentation Maintenance
### Review Schedule
- **Weekly**: Update in-progress features
- **Bi-weekly**: Review completed sections
- **Monthly**: Full documentation audit
- **Quarterly**: Archive outdated docs
### When to Update
- ✏️ New feature added → Add sitemap, flow, specs
- 🎨 Visual redesign → Update mockups, components
- 🔧 Component changes → Update specs, Storybook
- 🐛 Bug fix → Update known issues
- 🚀 New page → Add to sitemap, create flow
### Version Control
- Use semantic versioning (v1.0.0)
- Track in individual doc files
- Major changes → Update version in this README
- Keep changelog in relevant docs
---
## 📞 Contact & Support
**Design Team**: design@goodgo.com
**Dev Team**: dev@goodgo.com
**Product Team**: product@goodgo.com
**Slack Channels**:
- #design - Design discussions
- #dev-frontend - Development questions
- #accessibility - A11y topics
---
## 🔗 External Resources
### Design Tools
- [Figma Project](#) - Main design files
- [Storybook](http://localhost:6006) - Component library
- [Chromatic](#) - Visual regression testing
### Development Tools
- [GitHub Repo](#) - Source code
- [Vercel Dashboard](#) - Deployments
- [Sentry](#) - Error monitoring
### Learning Resources
- [X.ai Brand Guidelines](https://x.ai/legal/brand-guidelines)
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [React Aria Docs](https://react-spectrum.adobe.com/react-aria/)
---
## 📈 Metrics & Analytics
### Design System Adoption
- Components documented: 3/20 (15%)
- User flows documented: 1/10 (10%)
- Pages with specs: 3/3 (100% - Auth pages)
### Goals for Q1 2026
- [ ] 100% of auth flow documented
- [ ] All shared components have specs
- [ ] Storybook coverage > 80%
- [ ] Accessibility score > 95 on all pages
---
## 🎓 Onboarding
### New Designers
1. Read [Design System](./DESIGN_SYSTEM.md)
2. Review [Sitemap](./ux/sitemap.md)
3. Explore Figma files
4. Check existing [User Flows](./ux/flows/)
5. Review component specs in Storybook
### New Developers
1. Read [Auth Pages Implementation](./implementation/auth-pages-implementation.md)
2. Run `npm run storybook` to see components
3. Review [Theme CSS](../styles/theme.css)
4. Check [WCAG Compliance](./WCAG_COMPLIANCE.md)
5. Run tests: `npm test`
### New QA Engineers
1. Review all user flows in [ux/flows/](./ux/flows/)
2. Check testing checklists in implementation docs
3. Review [WCAG Compliance](./WCAG_COMPLIANCE.md)
4. Familiarize with Storybook for component testing
---
## 📋 Templates Available
| Template | Purpose | Location |
|----------|---------|----------|
| User Flow | Document user journeys | [TEMPLATE_USER_FLOW.md](./TEMPLATE_USER_FLOW.md) |
| Component Spec | Document UI components | [TEMPLATE_COMPONENT_SPEC.md](./TEMPLATE_COMPONENT_SPEC.md) |
---
## 🏆 Best Practices
### For Documentation
- ✅ Write clear, concise descriptions
- ✅ Include visual examples (screenshots, diagrams)
- ✅ Link related documents
- ✅ Keep it up-to-date
- ✅ Use consistent formatting
- ❌ Don't duplicate information
- ❌ Don't use jargon without explanation
### For Design
- ✅ Follow design system tokens
- ✅ Maintain accessibility standards
- ✅ Test on all breakpoints
- ✅ Document all states
- ✅ Get feedback early and often
### For Development
- ✅ Reference design specs
- ✅ Use CSS variables, not hardcoded values
- ✅ Test accessibility
- ✅ Write Storybook stories
- ✅ Keep components reusable
---
**Last Updated**: 2026-01-04
**Version**: 2.0.0 (X.ai Minimal Redesign)
**Maintained by**: Design Team + Dev Team

View File

@@ -1,422 +0,0 @@
# Component Specification Template
> **Copy this template for new component documentation**
>
> Location: `src/docs/ui/components/[component-name]-spec.md`
## 📦 Component: [Component Name]
**Path**: `src/features/[feature]/components/[component-name]/`
**Purpose**: [What does this component do? One-line description]
**Category**: [Form Input | Button | Layout | Navigation | Data Display | Feedback]
---
## 🎨 Visual Design
### Desktop View
![Desktop Screenshot](path/to/screenshot-desktop.png)
### Mobile View
![Mobile Screenshot](path/to/screenshot-mobile.png)
### Figma Link
[Link to Figma component]
---
## 🔧 Component API
### Props
| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `variant` | `'primary' \| 'secondary'` | `'primary'` | No | Visual style variant |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | No | Size of the component |
| `disabled` | `boolean` | `false` | No | Disable interaction |
| `children` | `ReactNode` | - | Yes | Content to display |
| `className` | `string` | - | No | Additional CSS classes |
| `onClick` | `() => void` | - | No | Click handler |
### Variants
#### Primary Variant
- **Use when**: [When to use this variant]
- **Visual**: [Description of appearance]
- **Example**:
```tsx
<Component variant="primary">Primary</Component>
```
#### Secondary Variant
- **Use when**: [When to use this variant]
- **Visual**: [Description of appearance]
- **Example**:
```tsx
<Component variant="secondary">Secondary</Component>
```
### Sizes
#### Small (`sm`)
- Height: [px/rem]
- Padding: [px/rem]
- Font size: [px/rem]
- Use case: [When to use]
#### Medium (`md`)
- Height: [px/rem]
- Padding: [px/rem]
- Font size: [px/rem]
- Use case: [When to use]
#### Large (`lg`)
- Height: [px/rem]
- Padding: [px/rem]
- Font size: [px/rem]
- Use case: [When to use]
---
## 📱 States
### Default State
**Description**: Normal, inactive state
**Visual**:
- Background: [color/variable]
- Border: [color/variable]
- Text: [color/variable]
**Code**:
```tsx
<Component>Default</Component>
```
---
### Hover State
**Description**: Mouse over the component
**Visual**:
- Background: [color/variable]
- Border: [color/variable]
- Text: [color/variable]
- Transform: [scale/translate if any]
**Code**:
```tsx
<Component className="hover:...">Hover me</Component>
```
---
### Active State
**Description**: Component is being clicked/pressed
**Visual**:
- Background: [color/variable]
- Border: [color/variable]
- Text: [color/variable]
---
### Focus State
**Description**: Component is focused (keyboard navigation)
**Visual**:
- Outline/Ring: [color/variable]
- Background: [color/variable]
**Accessibility**: Must be clearly visible for keyboard users
---
### Disabled State
**Description**: Component cannot be interacted with
**Visual**:
- Opacity: [value]
- Cursor: not-allowed
- Background: [color/variable]
**Code**:
```tsx
<Component disabled>Disabled</Component>
```
---
### Loading State (if applicable)
**Description**: Component is processing an action
**Visual**:
- Spinner/loading indicator
- Disabled interaction
- Optional text change
**Code**:
```tsx
<Component loading>Loading...</Component>
```
---
### Error State (if applicable)
**Description**: Component has validation error
**Visual**:
- Border: error color
- Background: error color with low opacity
- Error icon (optional)
- Error message below
**Code**:
```tsx
<Component error="Error message">Value</Component>
```
---
## 💡 Usage Examples
### Basic Usage
```tsx
import { Component } from '@/features/shared/components/ui/component'
function Example() {
return (
<Component>
Basic example
</Component>
)
}
```
---
### With All Props
```tsx
<Component
variant="primary"
size="lg"
disabled={false}
onClick={handleClick}
className="custom-class"
>
Full example
</Component>
```
---
### Complex Example
```tsx
function ComplexExample() {
const [loading, setLoading] = useState(false)
const handleClick = async () => {
setLoading(true)
await someAction()
setLoading(false)
}
return (
<Component
variant="primary"
size="lg"
loading={loading}
onClick={handleClick}
>
{loading ? 'Processing...' : 'Click me'}
</Component>
)
}
```
---
## 🎨 Design Tokens Used
### Colors
- `--accent-primary`: Primary action color
- `--bg-secondary`: Background color
- `--text-primary`: Text color
- `--border-primary`: Border color
### Typography
- Font: `var(--font-sans)`
- Size: `var(--text-base)`
- Weight: `var(--font-medium)`
### Spacing
- Padding: `var(--spacing-4)`
- Margin: `var(--spacing-2)`
### Borders
- Radius: `var(--radius-md)`
- Width: `1px`
### Shadows
- Default: `var(--shadow-sm)`
- Hover: `var(--shadow-md)`
---
## ♿ Accessibility
### ARIA Attributes
```tsx
<Component
role="button"
aria-label="Descriptive label"
aria-disabled={disabled}
>
Content
</Component>
```
### Keyboard Navigation
- **Tab**: Focus the component
- **Enter/Space**: Activate the component
- **Escape**: Cancel (if applicable)
### Screen Reader Support
- Component announces its purpose
- State changes are announced
- Error messages are announced with `role="alert"`
### Focus Management
- Visible focus indicator (X.ai blue ring)
- Focus trap (if modal/dialog)
- Focus returns to trigger element after close
### Color Contrast
- Text to background: ≥ 4.5:1 (WCAG AA)
- Focus indicator: ≥ 3:1 (WCAG AA)
---
## 📱 Responsive Behavior
### Mobile (< 640px)
- [How does it adapt?]
- [Touch target size: min 44x44px]
- [Full width or stacked?]
### Tablet (640-1024px)
- [How does it adapt?]
### Desktop (> 1024px)
- [How does it adapt?]
---
## 🧪 Testing
### Unit Tests
```tsx
describe('Component', () => {
it('renders correctly', () => {
// Test case
})
it('handles click events', () => {
// Test case
})
it('shows disabled state', () => {
// Test case
})
})
```
### Visual Regression Tests
- Default state
- All variants
- All sizes
- All states (hover, active, focus, disabled)
- Dark + Light theme
### Accessibility Tests
- axe-core tests pass
- Keyboard navigation works
- Screen reader announces correctly
---
## 🐛 Known Issues
### Issue 1: [Issue Description]
**Browsers affected**: [Chrome, Firefox, Safari, etc.]
**Workaround**: [How to work around it]
**Tracking**: [Link to GitHub issue]
---
## 📝 Development Notes
### Dependencies
- `react`: ^18.2.0
- `react-aria`: ^3.45.0 (if applicable)
- [Other dependencies]
### File Structure
```
component-name/
├── index.ts # Barrel export
├── component-name.tsx # Main component
├── component-name.test.tsx
├── component-name.stories.tsx
└── component-name.module.css (if needed)
```
### CSS Classes
```tsx
// Tailwind classes used
className="
bg-accent-primary
text-white
px-4 py-2
rounded-md
hover:bg-accent-primary-hover
focus:ring-2 focus:ring-accent-primary/30
disabled:opacity-50
transition-colors duration-150
"
```
---
## 🔄 Changelog
### v2.0.0 (2026-01-04)
- Updated to X.ai blue accent color
- Removed gradient, now solid color
- Updated focus states
### v1.0.0 (2025-12-01)
- Initial release
---
## 📚 Related Components
- [Related Component 1](./related-component-1-spec.md)
- [Related Component 2](./related-component-2-spec.md)
---
## 🔗 Links
- [Storybook](http://localhost:6006/?path=/story/component)
- [Figma Design](#)
- [GitHub Source](path/to/component)
---
**Maintained by**: [Team Name]
**Last Updated**: [Date]
**Version**: [Version Number]

View File

@@ -1,189 +0,0 @@
# User Flow Template
> **Copy this template to create new user flows**
>
> Location: `src/docs/ux/flows/[feature-name]-flow.md`
## 📋 Flow Overview
**Goal**: [What does the user want to accomplish?]
**Entry Points**:
- [Where can users start this flow?]
- [List all entry points]
**Exit Points**:
- Success → [Where do they go on success?]
- Cancel → [What happens if they cancel?]
- Error → [What happens on error?]
---
## 🗺️ Flow Diagram
```
┌─────────────┐
│ Entry Point │
└──────┬──────┘
v
┌──────────────────────────────┐
│ Step 1: [Action] │
│ - Detail 1 │
│ - Detail 2 │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ Step 2: [Action] │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ Step 3: [Decision Point] │
└──────┬───────────────────────┘
├─────────────┬──────────────┐
v Path A v Path B v Path C
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Outcome A │ │ Outcome B │ │ Outcome C │
└─────────────┘ └──────────────┘ └──────────────┘
```
---
## 📝 Detailed Steps
### Step 1: [Step Name]
**Action**: [What does the user do?]
**UI State**:
- [What's visible on screen?]
- [What's the initial state?]
**Validation** (if applicable):
- ✅ [Validation rule 1]
- ✅ [Validation rule 2]
**Error Messages**:
- [Error condition]: "[Error message]"
**Technical Notes**:
- [API calls, state changes, etc.]
---
### Step 2: [Step Name]
**Action**: [What does the user do?]
**UI State**:
- [What changes?]
**Behavior**:
- [How does it behave?]
---
### Step 3: [Decision Point]
**Action**: [What triggers the decision?]
**Possible Outcomes**:
#### ✅ Success Path
**Condition**: [When does this happen?]
**Actions**:
1. [What happens first?]
2. [What happens next?]
3. [Final outcome]
---
#### ❌ Error Path
**Condition**: [When does this happen?]
**Actions**:
1. [How is error shown?]
2. [How can user recover?]
---
## 🔀 Alternative Paths
### Path A: [Alternative Name]
**Trigger**: [What triggers this path?]
**Action**: [What happens?]
---
### Path B: [Alternative Name]
**Trigger**: [What triggers this path?]
**Action**: [What happens?]
---
## ✅ Success Criteria
Flow is successful when:
- ✅ [Criterion 1]
- ✅ [Criterion 2]
- ✅ [Criterion 3]
---
## 🧪 Testing Checklist
**Functional Tests**:
- [ ] [Test case 1]
- [ ] [Test case 2]
- [ ] [Test case 3]
**Edge Cases**:
- [ ] [Edge case 1]
- [ ] [Edge case 2]
**Accessibility**:
- [ ] Keyboard navigation works
- [ ] Screen reader announces correctly
- [ ] Focus management is correct
**Performance**:
- [ ] Page loads < [X]s
- [ ] API response < [X]s
- [ ] No layout shift
---
## 📱 Responsive Behavior
**Mobile (< 640px)**:
- [How does it adapt?]
**Tablet (640-1024px)**:
- [How does it adapt?]
**Desktop (> 1024px)**:
- [How does it adapt?]
---
## 🎨 Figma Prototype
[Link to Figma prototype]
---
## 📊 Analytics Events
Track these events:
- `[event_name_1]`
- `[event_name_2]`
- `[event_name_3]`
---
**Maintained by**: [Team Name]
**Last Updated**: [Date]
**Version**: [Version Number]

View File

@@ -1,53 +0,0 @@
# WCAG 2.1 AA Compliance Documentation
## Color Contrast
### Text Colors (WCAG 2.1 AA Compliant)
**Normal Text (< 18px): Minimum 4.5:1 contrast ratio**
- `--text-primary: #FAFAFA` on `--bg-primary: #0A0A0A` = **15.8:1**
- `--text-secondary: #E0E0E0` on `--bg-primary: #0A0A0A` = **12.6:1**
- `--text-tertiary: #A0A0A0` on `--bg-primary: #0A0A0A` = **6.4:1**
**Large Text (≥ 18px): Minimum 3:1 contrast ratio**
- All text colors meet this requirement ✓
### UI Components: Minimum 3:1 contrast ratio
- `--accent-primary: #3B82F6` on `--bg-primary: #0A0A0A` = **4.2:1**
- `--accent-success: #10B981` on `--bg-primary: #0A0A0A` = **5.1:1**
- `--accent-error: #EF4444` on `--bg-primary: #0A0A0A` = **5.8:1**
## Font Size
- **Minimum base font size: 16px** (set in `globals.css`)
- All text uses relative units (rem) for proper scaling
- Supports zoom up to 200% without breaking layout
## Keyboard Navigation
- **Focus indicators**: 2px solid outline with 2px offset
- **Tab order**: Logical and consistent
- **Custom shortcuts**: Ctrl+K (search), Ctrl+N (new chat), Ctrl+/ (help)
## Screen Reader Support
- **Semantic HTML**: Proper use of `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>`
- **ARIA labels**: All interactive elements have descriptive labels
- **Live regions**: Announcements with `role="status" aria-live="polite"`
- **Skip link**: "Skip to main content" link for keyboard navigation
## Alternative Text
- All images have descriptive `alt` attributes
- Decorative images use `alt=""`
- Avatar images include user names in alt text
## Testing Tools
- WebAIM Contrast Checker
- Chrome DevTools Accessibility tab
- WAVE browser extension
- axe DevTools

View File

@@ -1,477 +0,0 @@
# Báo Cáo Tình Trạng Implementation - Authentication Pages
**Ngày kiểm tra**: 2026-01-05
**Người kiểm tra**: AI Assistant
---
## 📊 Tổng Quan
| Tiêu chí | Trạng thái | Ghi chú |
|----------|-----------|---------|
| **Login Page** | ✅ Hoàn thành 100% | Đã implement đầy đủ theo design guide |
| **Register Page** | ✅ Hoàn thành 100% | Đã implement đầy đủ theo design guide |
| **Forgot Password** | ✅ Hoàn thành 100% | Đã implement đầy đủ theo design guide |
| **Theme CSS** | ✅ Hoàn thành 100% | Đã cập nhật X.ai minimal colors |
| **Glass CSS** | ✅ Hoàn thành 100% | Đã fix shadow values và X.ai blue glow |
| **Input Component** | ✅ Hoàn thành 100% | X.ai blue focus ring đúng 100% |
| **Button Component** | ✅ Hoàn thành 100% | Solid X.ai blue (no gradient) |
**🎉 HOÀN THÀNH 100%** - Tất cả components đã implement đúng theo X.ai minimal design guide!
---
## ✅ Đã Hoàn Thành
### 1. Login Page (`/login`)
**File**: `src/app/(auth)/login/page.tsx`
**Background**:
- Line 113: Đã đổi từ `bg-black``bg-bg-primary` (#15202b)
- Đã xóa cosmic background (lines 115-116 có comment)
**Icon Container**:
- Line 123: Đã cập nhật `bg-accent-primary/5 border border-accent-primary/10`
- Đã xóa `animate-float` class (icon tĩnh)
**Links & Buttons**:
- Line 237: "Forgot password" link dùng `text-accent-primary hover:text-accent-primary-hover`
- Line 264: "Sign up" link dùng `text-accent-primary hover:text-accent-primary-hover`
- Line 246-257: Button dùng `variant="brand"`
**Form Elements**:
- Line 226: Checkbox dùng `text-accent-primary focus:ring-accent-primary`
- Inputs sử dụng Input component với validation
---
### 2. Register Page (`/register`)
**File**: `src/app/(auth)/register/page.tsx`
**Background**:
- Line 230: Đã đổi từ `bg-black``bg-bg-primary` (#15202b)
- Đã xóa cosmic background (lines 232-233 có comment)
**Icon Container**:
- Line 240: Đã cập nhật `bg-accent-primary/5 border border-accent-primary/10`
- Đã xóa `animate-float` class (icon tĩnh)
**Password Strength Indicator**:
- Lines 355-383: Đã implement đầy đủ với 4 bars
- Sử dụng màu X.ai: error (red), warning (amber), success (green)
- Real-time calculation với `useMemo`
**Links & Buttons**:
- Line 426: Terms link dùng `text-accent-primary hover:text-accent-primary-hover`
- Line 466: "Sign in" link dùng `text-accent-primary hover:text-accent-primary-hover`
- Line 448-459: Button dùng `variant="brand"`
**Form Elements**:
- Line 418: Terms checkbox dùng `text-accent-primary focus:ring-accent-primary`
- Inputs sử dụng Input component với validation đầy đủ
---
### 3. Forgot Password Page (`/forgot-password`)
**File**: `src/app/(auth)/forgot-password/page.tsx`
**Background**:
- Line 113: Đã đổi từ `bg-black``bg-bg-primary` (#15202b)
- Đã xóa cosmic background (lines 115-116 có comment)
**Icon Container**:
- Line 123: Đã cập nhật `bg-accent-primary/5 border border-accent-primary/10`
- Đã xóa `animate-float` class (icon tĩnh)
**Success State**:
- Lines 152-211: Đã implement đầy đủ success state
- Success icon với green color
- "Back to login" button
- "Send to another email" option
**Links & Buttons**:
- Line 274: "Back to login" link dùng `text-accent-primary hover:text-accent-primary-hover`
- Line 192-199: Button dùng `variant="brand"`
**Form Elements**:
- Line 240-256: Input sử dụng Input component với validation
---
### 4. Theme CSS
**File**: `src/styles/theme.css`
**Background Colors** (Lines 22-30):
```css
--bg-primary: #15202b; /* ✅ Warm dark gray */
--bg-secondary: #1a2734; /* ✅ Lighter variant */
--bg-tertiary: #1f2f3d; /* ✅ Medium variant */
--bg-elevated: #243442; /* ✅ Elevated variant */
```
**Accent Colors** (Lines 44-50):
```css
--accent-primary: #1D9BF0; /* ✅ X.ai blue */
--accent-primary-hover: #1a8cd8; /* ✅ X.ai blue hover */
--accent-primary-light: #8ecdf7; /* ✅ X.ai blue light */
```
**Brand Colors** (Lines 89-97):
```css
--brand-primary: #1D9BF0; /* ✅ X.ai blue */
--brand-primary-light: #8ecdf7; /* ✅ Light variant */
--brand-primary-dark: #1a8cd8; /* ✅ Dark variant */
```
**Border Colors** (Lines 76-82):
```css
--border-focus: #1D9BF0; /* ✅ X.ai blue for focus */
```
**Glass Effects** (Lines 113-143):
```css
--glass-bg-default: rgba(255, 255, 255, 0.04); /* ✅ 4% opacity */
--glass-border-default: rgba(255, 255, 255, 0.08); /* ✅ 8% opacity */
--glass-border-focus: rgba(29, 155, 240, 0.5); /* ✅ X.ai blue 50% */
--glass-blur-md: 8px; /* ✅ Reduced blur */
```
---
## ✅ Đã Kiểm Tra - Components
### 1. Glass CSS File ⚠️ CẦN CẬP NHẬT
**File**: `src/styles/glass.css`
**Tình trạng**: ❌ **Chưa đúng theo design guide**
**Hiện tại** (Lines 45-59):
```css
.glass-card {
background: var(--glass-bg-default);
backdrop-filter: blur(var(--glass-blur-sm)); /* ✅ Đúng */
border: 1px solid var(--glass-border-default); /* ✅ Đúng */
box-shadow: var(--shadow); /* ⚠️ Cần update */
border-radius: var(--radius-lg);
transition: all var(--duration-fast) var(--ease-snap);
}
.glass-card:hover {
transform: translateY(-1px); /* ⚠️ Không cần transform */
border-color: var(--glass-border-hover); /* ✅ Đúng */
/* No shadow change - cleaner look */ /* ❌ Cần thêm shadow */
}
```
**Cần sửa thành** (theo design guide):
```css
.glass-card {
background: var(--glass-bg-default);
backdrop-filter: blur(var(--glass-blur-sm));
border: 1px solid var(--glass-border-default);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* Softer shadow */
border-radius: var(--radius-lg);
}
.glass-card:hover {
border-color: var(--glass-border-hover);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); /* Softer hover */
}
```
**Glass Input** (Lines 108-125):
```css
.glass-input:focus {
background: var(--glass-bg-default);
border-color: var(--glass-border-focus); /* ✅ Đúng (X.ai blue) */
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.03); /* ❌ Cần X.ai blue glow */
outline: none;
}
```
**Cần sửa thành**:
```css
.glass-input:focus {
border-color: var(--accent-primary); /* X.ai blue */
box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1); /* X.ai blue glow */
}
```
---
### 2. Input Component ✅ HOÀN THÀNH
**File**: `src/features/shared/components/ui/input/input.tsx`
**Tình trạng**: ✅ **Đã đúng 100% theo design guide**
**Focus States** (Lines 203-206):
```tsx
// ✅ Đã implement đúng X.ai blue focus ring
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'focus:ring-accent-primary/30 focus:ring-offset-bg-primary',
'focus:bg-glass focus:border-accent-primary',
```
**Khớp với design guide**:
-`focus:ring-accent-primary/30` - X.ai blue ring 30% opacity
-`focus:ring-offset-bg-primary` - Ring offset với background
-`focus:border-accent-primary` - Border X.ai blue khi focus
**Validation States** (Lines 208-210):
```tsx
// ✅ Invalid state với accent-error
'data-[invalid]:border-accent-error',
'data-[invalid]:focus:ring-accent-error/50',
```
**Kết luận**: ✅ Input component đã hoàn thành đúng theo X.ai minimal design
---
### 3. Button Component ✅ HOÀN THÀNH
**File**: `src/features/shared/components/ui/button/button.tsx`
**Tình trạng**: ✅ **Đã đúng 100% theo design guide**
**Brand Variant** (Lines 82-90):
```tsx
// ✅ Đã implement đúng X.ai blue solid color
brand: [
'bg-accent-primary text-white', // ✅ Solid X.ai blue (không có gradient)
'shadow-md hover:shadow-lg', // ✅ Simplified shadow
'hover:bg-accent-primary-hover', // ✅ X.ai blue hover
'transition-colors duration-quick', // ✅ Fast transition
'focus-visible:ring-2 focus-visible:ring-accent-primary/30', // ✅ X.ai blue focus ring
'focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary',
],
```
**Khớp với design guide**:
- ✅ Gradient → Solid X.ai blue (`bg-accent-primary`)
- ✅ Simplified hover (no scale transform) - chỉ có `hover:bg-accent-primary-hover`
- ✅ X.ai blue focus ring (`focus-visible:ring-accent-primary/30`)
- ✅ Fast transition (`duration-quick`)
**Active State** (Line 35):
```tsx
'active:scale-[0.98]', // ✅ Minimal press feedback
```
**Kết luận**: ✅ Button component đã hoàn thành đúng theo X.ai minimal design
---
## 🧪 Testing Checklist
### Visual Testing (Dark Theme)
- [ ] Background là #15202b (warm dark gray)
- [ ] Không có cosmic blur orbs
- [ ] Icon container có X.ai blue tint
- [ ] Primary button là X.ai blue
- [ ] Links là X.ai blue
- [ ] Input focus ring là X.ai blue
- [ ] Glass effects subtle nhưng visible
- [ ] Text readable (high contrast)
### Visual Testing (Light Theme)
- [ ] Background là white
- [ ] X.ai blue vẫn prominent
- [ ] Text là dark
- [ ] Tất cả interactive elements visible
### Accessibility Testing
- [ ] Lighthouse Accessibility score ≥ 95
- [ ] Contrast ratio ≥ 4.5:1 cho tất cả text
- [ ] Focus indicators visible (X.ai blue ring)
- [ ] Keyboard navigation works
- [ ] Screen reader announces form fields correctly
- [ ] Error messages có `role="alert"`
- [ ] Form labels properly associated
### Responsive Testing
- [ ] Form centered trên tất cả screen sizes
- [ ] Text readable without zoom trên mobile
- [ ] Buttons touch-friendly (min 44x44px)
- [ ] Không có horizontal scroll
- [ ] AuthControls accessible trên mobile
- [ ] Virtual keyboard không hide submit button
### Functional Testing
#### Login Page
- [ ] Submit với valid email + password
- [ ] Email validation shows error
- [ ] Password validation shows error
- [ ] "Remember me" checkbox works
- [ ] "Forgot password" link navigates
- [ ] "Sign up" link navigates
- [ ] Loading state shows spinner
- [ ] API error displays message
#### Register Page
- [ ] Password strength indicator updates real-time
- [ ] Confirm password validates match
- [ ] Terms checkbox required
- [ ] All validation messages display
#### Forgot Password
- [ ] Email validation works
- [ ] Success state shows confirmation
- [ ] "Back to login" link works
- [ ] "Send to another email" works
### Cross-Browser Testing
- [ ] Chrome (latest)
- [ ] Firefox (latest)
- [ ] Safari (latest)
- [ ] Edge (latest)
## 📝 Kết Luận
### Tổng Kết
**🎉 HOÀN THÀNH 100%** ✅
**Đã làm xong** (100%):
1.**Login Page** - 100% hoàn thành
2.**Register Page** - 100% hoàn thành
3.**Forgot Password Page** - 100% hoàn thành
4.**Theme CSS** - 100% hoàn thành (X.ai minimal colors)
5.**Glass CSS** - 100% hoàn thành (đã fix shadow values và X.ai blue glow)
6.**Input Component** - 100% hoàn thành (X.ai blue focus ring)
7.**Button Component** - 100% hoàn thành (solid X.ai blue, no gradient)
### Chi Tiết Đã Fix
#### File: `src/styles/glass.css` ✅
**1. Glass Card Shadow (Line 50)**
✅ Đã sửa từ:
```css
box-shadow: var(--shadow);
```
Thành:
```css
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* Softer shadow */
```
**2. Glass Card Hover (Lines 55-59)**
✅ Đã xóa:
```css
transform: translateY(-1px); /* Removed */
```
✅ Đã thêm:
```css
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); /* Softer hover */
```
**3. Glass Input Focus (Line 123)**
✅ Đã sửa từ:
```css
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.03); /* White glow */
```
Thành:
```css
box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1); /* X.ai blue glow */
```
### Next Steps - Testing
#### 1. Visual Testing (10 phút)
```bash
# Dev server đang chạy tại http://localhost:3000
# Navigate to:
# - http://localhost:3000/login
# - http://localhost:3000/register
# - http://localhost:3000/forgot-password
```
**Kiểm tra**:
- [ ] Glass card có shadow mềm hơn (0 2px 8px)
- [ ] Glass card hover có shadow tăng nhẹ (0 4px 12px)
- [ ] Input focus có X.ai blue glow (không phải white)
- [ ] Tất cả buttons là X.ai blue solid
- [ ] Links là X.ai blue
- [ ] Background là #15202b (warm dark gray)
- [ ] Không có cosmic blur orbs
#### 2. Accessibility Testing (10 phút)
**Tools**:
- Chrome DevTools Lighthouse
- axe DevTools extension
**Targets**:
- [ ] Lighthouse Accessibility score ≥ 95
- [ ] Contrast ratio ≥ 4.5:1 (X.ai blue #1D9BF0 on #15202b = 5.2:1 ✅)
- [ ] Focus indicators visible
- [ ] Keyboard navigation works
#### 3. Responsive Testing (10 phút)
**Breakpoints**:
- [ ] Mobile: 375px (iPhone)
- [ ] Tablet: 768px (iPad)
- [ ] Desktop: 1440px
### Đánh Giá Chất Lượng
| Tiêu chí | Điểm | Ghi chú |
|----------|------|---------|
| **Code Quality** | 10/10 | ✅ Perfect - Clean, maintainable code |
| **Design Compliance** | 10/10 | ✅ 100% khớp với X.ai minimal design |
| **Accessibility** | 10/10 | ✅ Đầy đủ ARIA, keyboard nav, focus states |
| **Performance** | 10/10 | ✅ Minimal blur, fast transitions |
| **Maintainability** | 10/10 | ✅ Clean code, good comments |
### Kết Luận Cuối
**🎉 Implementation đã hoàn thành 100%** với chất lượng cao!
**Điểm mạnh**:
- ✅ Tất cả 3 trang auth đã implement đúng design guide
- ✅ Theme CSS hoàn hảo với X.ai minimal colors (#15202b, #1D9BF0)
- ✅ Glass CSS đã fix đúng shadow values và X.ai blue glow
- ✅ Input và Button components đã đúng 100%
- ✅ Code quality cao, maintainable
- ✅ Accessibility đầy đủ
- ✅ Performance tối ưu (minimal blur, fast transitions)
**Thay đổi chính**:
1. Background: `#000000``#15202b` (warm dark gray)
2. Accent: `#FFFFFF``#1D9BF0` (X.ai blue)
3. Cosmic effects: Removed (minimal design)
4. Glass shadows: Softer values
5. Focus glow: X.ai blue (not white)
**Khuyến nghị tiếp theo**:
1. ✅ Chạy visual testing (10 phút)
2. ✅ Chạy accessibility testing (10 phút)
3. ✅ Chạy responsive testing (10 phút)
4. ✅ Deploy to staging để QA test
---
**Tác giả**: AI Assistant
**Ngày tạo**: 2026-01-05
**Ngày hoàn thành**: 2026-01-05 10:16
**Version**: 3.0 (100% Complete)

View File

@@ -1,594 +0,0 @@
# Authentication Pages - Implementation Guide
> **Developer Handoff Documentation**
>
> Hướng dẫn chi tiết để implement 3 trang xác thực theo X.ai minimal design
## 🎯 Design Intent
**Vision**: Tạo trải nghiệm xác thực minimal, sophisticated, và user-friendly theo phong cách X.ai 2026
**Key Principles**:
- **Neo-minimalism**: "Less is enough" - loại bỏ yếu tố thừa
- **High contrast**: White text on warm dark background
- **Accessibility-first**: WCAG 2.1 AA compliance
- **Responsive**: Mobile-first approach
- **Performance**: Fast load, smooth animations
---
## 📋 Pages Overview
### 1. Login Page (`/login`)
- Email + Password form
- Remember me checkbox
- Forgot password link
- Sign up link
### 2. Register Page (`/register`)
- Email + Password + Confirm Password
- Password strength indicator
- Terms & conditions checkbox
- Sign in link
### 3. Forgot Password Page (`/forgot-password`)
- Email input
- Success state with confirmation
- Back to login link
---
## 🎨 Design Changes (X.ai Minimal Redesign)
### Background
**Before**: Pure black (#000000) with 2 floating blur orbs
**After**: Warm dark gray (#15202b), clean solid background
**Rationale**:
- #15202b reduces eye strain
- Softer, more inviting feel
- Aligns with X.ai's 2026 neo-minimalism
- Better for OLED displays
---
### Accent Color
**Before**: White (#FFFFFF)
**After**: X.ai blue (#1D9BF0)
**Usage**:
- Primary buttons
- Links
- Focus states
- Interactive elements
**Rationale**:
- Creates strong brand identity
- Better visual hierarchy
- Meets WCAG contrast requirements on #15202b
---
### Cosmic Background Removal
**Removed**:
```tsx
// DELETE THIS:
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50" />
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50" />
</div>
```
**Rationale**:
- Reduce visual noise
- Focus user attention on form
- Faster rendering (no blur filters)
- Aligns with minimal aesthetic
---
## 🛠️ Technical Implementation
### Step 1: Update Theme Variables
**File**: `src/styles/theme.css`
**Changes**:
```css
/* Dark Theme Colors */
:root {
/* Backgrounds */
--bg-primary: #15202b; /* Changed from #000000 */
--bg-secondary: #1a2734; /* Changed from #0A0A0A */
--bg-tertiary: #1f2f3d; /* Changed from #141414 */
--bg-elevated: #243442; /* Changed from #1A1A1A */
/* Accent Colors */
--accent-primary: #1D9BF0; /* Changed from #FFFFFF */
--accent-primary-hover: #1a8cd8;
/* Brand Colors */
--brand-primary: #1D9BF0; /* Changed from #FFFFFF */
--brand-primary-light: #8ecdf7;
--brand-primary-dark: #1a8cd8;
/* Borders */
--border-focus: #1D9BF0; /* Changed from #FFFFFF */
/* Glass Effects */
--glass-border-focus: rgba(29, 155, 240, 0.5);
}
/* Light Theme */
[data-theme="light"], .light {
--bg-primary: #FFFFFF;
--accent-primary: #1D9BF0;
--border-focus: #1D9BF0;
}
```
**Impact**: All components using CSS variables automatically update
---
### Step 2: Update Glass Effects
**File**: `src/styles/glass.css`
**Changes**:
```css
.glass-card {
background: var(--glass-bg-default);
backdrop-filter: blur(var(--glass-blur-sm));
border: 1px solid var(--glass-border-default);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); /* Softer shadow */
border-radius: var(--radius-lg);
}
.glass-card:hover {
border-color: var(--glass-border-hover);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); /* Softer hover */
}
.glass-input:focus {
border-color: var(--accent-primary); /* X.ai blue */
box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1); /* X.ai blue glow */
}
```
---
### Step 3: Update Auth Pages
#### Login Page
**File**: `src/app/(auth)/login/page.tsx`
**Line 116** - Update background:
```tsx
// BEFORE:
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black py-12 px-4 sm:px-6 lg:px-8"
// AFTER:
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-bg-primary py-12 px-4 sm:px-6 lg:px-8"
```
**Lines 118-126** - Remove cosmic background:
```tsx
// DELETE ENTIRE BLOCK
```
**Lines 133-147** - Update icon container:
```tsx
// BEFORE:
<div className="p-3 rounded-2xl bg-white/5 border border-white/10 shadow-glass-sm animate-float">
// AFTER:
<div className="p-3 rounded-2xl bg-accent-primary/5 border border-accent-primary/10 shadow-glass-sm">
```
**Note**: Remove `animate-float` class for static icon
---
#### Register Page
**File**: `src/app/(auth)/register/page.tsx`
**Same changes as Login**:
1. Line 230: `bg-black``bg-bg-primary`
2. Lines 232-240: Delete cosmic background
3. Lines 247-261: Update icon container
---
#### Forgot Password Page
**File**: `src/app/(auth)/forgot-password/page.tsx`
**Same changes as Login**:
1. Line 115: `bg-black``bg-bg-primary`
2. Lines 118-125: Delete cosmic background
3. Lines 132-146: Update icon container
**Optional** - Line 278, update "Back to login" link:
```tsx
// BEFORE:
className="text-sm font-medium text-text-secondary hover:text-white transition-colors inline-flex items-center gap-2 group"
// AFTER:
className="text-sm font-medium text-accent-primary hover:text-accent-primary-hover transition-colors inline-flex items-center gap-2 group"
```
---
### Step 4: Update Input Component
**File**: `src/features/shared/components/ui/input/input.tsx`
**Lines 203-206** - Update focus states:
```tsx
// BEFORE:
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'focus:ring-glass-focus focus:ring-offset-bg-primary',
'focus:bg-glass focus:border-glass-hover',
// AFTER:
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'focus:ring-accent-primary/30 focus:ring-offset-bg-primary',
'focus:bg-glass focus:border-accent-primary',
```
**Visual Result**: X.ai blue focus ring around inputs
---
### Step 5: Update Button Component
**File**: `src/features/shared/components/ui/button/button.tsx`
**Lines 83-88** - Update brand variant:
```tsx
// BEFORE:
brand: [
'bg-brand-gradient text-white',
'shadow-brand hover:shadow-brand-lg',
'hover:scale-[1.02]',
'focus-visible:ring-brand-primary focus-visible:shadow-colored',
],
// AFTER:
brand: [
'bg-accent-primary text-white',
'shadow-md hover:shadow-lg',
'hover:bg-accent-primary-hover',
'focus-visible:ring-2 focus-visible:ring-accent-primary/30 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary',
],
```
**Changes**:
- Gradient → Solid X.ai blue
- Simplified hover (no scale transform)
- X.ai blue focus ring
---
## 🧪 Testing Requirements
### Visual Testing
**Dark Theme** (Default):
```bash
# Start dev server
npm run dev
# Navigate to:
http://localhost:3000/login
http://localhost:3000/register
http://localhost:3000/forgot-password
```
**Checklist**:
- [ ] Background is #15202b (warm dark gray)
- [ ] No cosmic blur orbs visible
- [ ] Icon container has X.ai blue tint
- [ ] Primary button is X.ai blue
- [ ] Links are X.ai blue
- [ ] Input focus ring is X.ai blue
- [ ] Glass effects subtle but visible
- [ ] Text readable (high contrast)
---
**Light Theme**:
```bash
# Click theme toggle in top-right
# Or manually:
localStorage.setItem('theme', 'light')
```
**Checklist**:
- [ ] Background is white
- [ ] X.ai blue still prominent
- [ ] Text is dark
- [ ] All interactive elements visible
---
### Accessibility Testing
**Tools**:
- Chrome DevTools Lighthouse
- axe DevTools extension
- WebAIM Contrast Checker
**Requirements**:
- [ ] Lighthouse Accessibility score ≥ 95
- [ ] Contrast ratio ≥ 4.5:1 for all text
- [ ] Focus indicators visible (X.ai blue ring)
- [ ] Keyboard navigation works (Tab, Shift+Tab, Enter)
- [ ] Screen reader announces form fields correctly
- [ ] Error messages have `role="alert"`
- [ ] Form labels properly associated
**Contrast Check**:
```
X.ai blue (#1D9BF0) on dark bg (#15202b):
✅ Contrast ratio: 5.2:1 (Passes WCAG AA)
White (#FFFFFF) on dark bg (#15202b):
✅ Contrast ratio: 12.8:1 (Passes WCAG AAA)
```
---
### Responsive Testing
**Breakpoints**:
```bash
# Mobile
Resize browser to 375px width (iPhone)
# Tablet
Resize to 768px width (iPad)
# Desktop
Resize to 1440px width
```
**Checklist**:
- [ ] Form centered on all screen sizes
- [ ] Text readable without zoom on mobile
- [ ] Buttons touch-friendly (min 44x44px)
- [ ] No horizontal scroll
- [ ] AuthControls (theme/lang) accessible on mobile
- [ ] Virtual keyboard doesn't hide submit button
---
### Functional Testing
**Login Page**:
- [ ] Can submit with valid email + password
- [ ] Email validation shows error for invalid format
- [ ] Password validation shows error if < 8 chars
- [ ] "Remember me" checkbox works
- [ ] "Forgot password" link navigates correctly
- [ ] "Sign up" link navigates correctly
- [ ] Loading state shows spinner
- [ ] API error displays message
**Register Page**:
- [ ] Password strength indicator updates in real-time
- [ ] Confirm password validates match
- [ ] Terms checkbox required to submit
- [ ] All validation messages display correctly
**Forgot Password**:
- [ ] Email validation works
- [ ] Success state shows confirmation
- [ ] "Back to login" link works
- [ ] "Send to another email" option works
---
### Cross-Browser Testing
**Required Browsers**:
- [ ] Chrome (latest)
- [ ] Firefox (latest)
- [ ] Safari (latest)
- [ ] Edge (latest)
**Known Issues**:
- Safari: backdrop-filter may have slight differences
- Solution: Fallback to solid background if needed
---
## 🎨 Design Specifications
### Layout Measurements
**Form Container**:
- Max-width: 448px (`max-w-md`)
- Padding: 32px (`p-8`)
- Border-radius: 12px (`rounded-xl`)
**Spacing**:
- Form fields: 16px gap (`space-y-4`)
- Sections: 24px gap (`space-y-6`)
- Button to form: 24px (`mt-6`)
**Typography**:
- Heading: 36px / 2.25rem (`text-4xl`)
- Body: 16px / 1rem (`text-base`)
- Small: 14px / 0.875rem (`text-sm`)
---
### Color Specifications
**Backgrounds**:
```
Dark Primary: #15202b
Dark Secondary: #1a2734
Light Primary: #FFFFFF
```
**Accent**:
```
X.ai Blue: #1D9BF0
X.ai Blue Hover: #1a8cd8
X.ai Blue Light: #8ecdf7
```
**Text**:
```
Primary: #FFFFFF (dark theme), #1D1D1F (light theme)
Secondary: #8899A6
Tertiary: #657786
```
**Status**:
```
Success: #10B981
Error: #EF4444
Warning: #F59E0B
```
---
### Animation Specifications
**Transitions**:
```css
Fast: 150ms cubic-bezier(0.4, 0.0, 0.2, 1)
Normal: 300ms cubic-bezier(0.4, 0.0, 0.2, 1)
```
**Hover Effects**:
- Button: `hover:bg-accent-primary-hover`
- Link: `hover:text-accent-primary-hover`
- Card: `hover:border-glass-hover`
**Focus Effects**:
- Ring: 2px solid, 30% opacity X.ai blue
- Offset: 2px from element
---
## 🐛 Common Issues & Solutions
### Issue 1: Glass effect not visible
**Cause**: Backdrop-filter not supported or disabled
**Solution**:
```css
/* Add fallback */
.glass-card {
background: rgba(255, 255, 255, 0.05); /* Fallback */
backdrop-filter: blur(8px);
}
@supports not (backdrop-filter: blur(8px)) {
.glass-card {
background: rgba(255, 255, 255, 0.1); /* More opaque */
}
}
```
---
### Issue 2: Focus ring not showing
**Cause**: Browser default outline removed
**Solution**: Always replace with custom focus styles:
```tsx
className="focus:outline-none focus:ring-2 focus:ring-accent-primary/30"
```
---
### Issue 3: Color contrast failure
**Cause**: Insufficient contrast ratio
**Solution**: Use CSS variables that are pre-validated:
```tsx
// ✅ Good
text-text-primary
// ❌ Bad
text-gray-400 // May not meet contrast requirements
```
---
## 📦 Assets Needed
### Icons
- [ ] Logo/Brand mark (SVG) - Already in codebase
- [ ] Social login icons (future)
### Images
- None required for auth pages (minimal design)
### Fonts
- [x] Inter (already loaded via Tailwind)
---
## 🔄 Migration Checklist
For developers implementing this redesign:
### Phase 1: Preparation
- [ ] Read this document fully
- [ ] Review Figma designs (if available)
- [ ] Backup current code (git branch)
- [ ] Run existing tests to ensure baseline
### Phase 2: Implementation
- [ ] Update `src/styles/theme.css`
- [ ] Update `src/styles/glass.css`
- [ ] Update `src/features/shared/components/ui/input/input.tsx`
- [ ] Update `src/features/shared/components/ui/button/button.tsx`
- [ ] Update `src/app/(auth)/login/page.tsx`
- [ ] Update `src/app/(auth)/register/page.tsx`
- [ ] Update `src/app/(auth)/forgot-password/page.tsx`
### Phase 3: Testing
- [ ] Visual testing (dark + light theme)
- [ ] Accessibility testing (Lighthouse)
- [ ] Responsive testing (mobile, tablet, desktop)
- [ ] Functional testing (all forms)
- [ ] Cross-browser testing
### Phase 4: Review
- [ ] Code review with team
- [ ] Design review with designer
- [ ] QA testing
- [ ] Performance benchmarks
### Phase 5: Deployment
- [ ] Merge to main branch
- [ ] Deploy to staging
- [ ] Final QA on staging
- [ ] Deploy to production
- [ ] Monitor for issues
---
## 📞 Support
**Questions about design**: Contact Design Team
**Questions about implementation**: Contact Dev Lead
**Bug reports**: Create issue in GitHub
---
## 📚 Related Documentation
- [Design System](../DESIGN_SYSTEM.md) - Full design system docs
- [WCAG Compliance](../WCAG_COMPLIANCE.md) - Accessibility guidelines
- [Login User Flow](../ux/flows/auth-login.md) - Detailed UX flow
---
**Author**: Design Team + Dev Team
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal Redesign)

View File

@@ -1,397 +0,0 @@
# Theme Completeness Report - X.ai Minimal Design
**Ngày kiểm tra**: 2026-01-05
**Phiên bản**: 1.0
---
## 📊 Tổng Quan
**Kết quả**: ✅ Theme đã hoàn chỉnh 98% - Chỉ cần vài improvements nhỏ
| Component | Status | Ghi chú |
|-----------|--------|---------|
| **Theme CSS** | ✅ 100% | Hoàn hảo - X.ai minimal colors |
| **Glass CSS** | ✅ 100% | Đã fix shadow values |
| **Tailwind Config** | ✅ 100% | Đầy đủ utilities |
| **Global CSS** | ✅ 100% | Base styles tốt |
| **Light Mode** | ⚠️ 90% | Cần test kỹ hơn |
| **Animations** | ⚠️ 95% | Có thể thêm vài animations |
---
## ✅ Điểm Mạnh
### 1. Theme Variables (theme.css)
**Hoàn hảo** - Đã có đầy đủ:
- ✅ X.ai minimal colors (#15202b, #1D9BF0)
- ✅ Glass effects với opacity phù hợp (4-8%)
- ✅ Typography scale đầy đủ
- ✅ Spacing system nhất quán
- ✅ Animation timing (fast, snappy)
- ✅ Dark/Light theme support
### 2. Tailwind Config
**Hoàn hảo** - Đã map tất cả CSS variables:
- ✅ Colors (bg, text, accent, brand, glass)
- ✅ Typography (font sizes, weights, line heights)
- ✅ Spacing & layout
- ✅ Shadows (glass shadows)
- ✅ Backdrop blur levels
- ✅ Animation utilities
### 3. Glass Effects
**Hoàn hảo** - Đã có đầy đủ glass utilities:
-`.glass-card` - với softer shadows
-`.glass-input` - với X.ai blue focus
-`.glass-button`
-`.glass-modal`
-`.glass-nav`
-`.glass-dropdown`
### 4. Accessibility
**Tốt** - Đã có:
- ✅ Focus indicators (X.ai blue outline)
- ✅ Skip links (.sr-only)
- ✅ Minimum font size (16px)
- ✅ WCAG 2.1 AA compliant colors
---
## ⚠️ Cần Bổ Sung (Optional Improvements)
### 1. Light Mode Testing ⚠️
**Hiện tại**: Đã có light mode variables nhưng chưa test kỹ
**Cần làm**:
```css
/* theme.css - Light mode đã có nhưng cần verify */
[data-theme="light"] {
--bg-primary: #FFFFFF;
--accent-primary: #1D9BF0;
--glass-bg-default: rgba(0, 0, 0, 0.04); /* Inverted for light */
}
```
**Action**: Test light mode trên auth pages để đảm bảo:
- [ ] Background trắng
- [ ] Text đen
- [ ] X.ai blue vẫn prominent
- [ ] Glass effects visible
---
### 2. Smooth Scroll Behavior 💡
**Thiếu**: Smooth scroll cho anchor links
**Đề xuất thêm vào `globals.css`**:
```css
@layer base {
html {
scroll-behavior: smooth;
}
/* Disable for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
}
```
**Lý do**: Cải thiện UX khi click anchor links
---
### 3. Selection Color 💡
**Thiếu**: Custom text selection color
**Đề xuất thêm vào `globals.css`**:
```css
@layer base {
::selection {
background-color: var(--accent-primary);
color: var(--text-primary);
}
::-moz-selection {
background-color: var(--accent-primary);
color: var(--text-primary);
}
}
```
**Lý do**: Brand consistency khi user select text
---
### 4. Loading States 💡
**Thiếu**: Skeleton loading animation
**Đề xuất thêm vào `globals.css`**:
```css
@keyframes skeleton {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@layer utilities {
.skeleton {
background: linear-gradient(
90deg,
var(--glass-bg-subtle) 0%,
var(--glass-bg-default) 50%,
var(--glass-bg-subtle) 100%
);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
}
}
```
**Lý do**: Better UX khi loading data
---
### 5. Scrollbar Styling 💡
**Thiếu**: Custom scrollbar theo X.ai theme
**Đề xuất thêm vào `globals.css`**:
```css
@layer base {
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border-default);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--glass-border-hover);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--glass-border-default) var(--bg-secondary);
}
}
```
**Lý do**: Consistent với X.ai minimal design
---
### 6. Print Styles 💡
**Thiếu**: Print-friendly styles
**Đề xuất thêm vào `globals.css`**:
```css
@media print {
body {
background: white;
color: black;
}
.glass-card {
background: white;
border: 1px solid #ccc;
box-shadow: none;
}
/* Hide non-essential elements */
nav, footer, .no-print {
display: none;
}
}
```
**Lý do**: Better UX khi user print pages
---
### 7. Focus-Within States 💡
**Thiếu**: Focus-within for form groups
**Đề xuất thêm vào `glass.css`**:
```css
.glass-card:focus-within {
border-color: var(--accent-primary);
box-shadow: 0 0 0 1px var(--accent-primary);
}
```
**Lý do**: Visual feedback khi focus vào form trong card
---
## 🎨 Đề Xuất Bổ Sung (Nice to Have)
### 1. Gradient Utilities (Minimal)
Mặc dù X.ai minimal không dùng nhiều gradient, nhưng có thể thêm vài gradients tinh tế:
```css
/* theme.css */
:root {
/* Subtle gradients for special cases */
--gradient-subtle: linear-gradient(
180deg,
var(--bg-primary) 0%,
var(--bg-secondary) 100%
);
--gradient-accent: linear-gradient(
135deg,
var(--accent-primary) 0%,
var(--accent-primary-hover) 100%
);
}
```
---
### 2. Micro-interactions
Thêm vài micro-animations cho buttons:
```css
@layer utilities {
.btn-press {
transition: transform var(--duration-fast) var(--ease-snap);
}
.btn-press:active {
transform: scale(0.98);
}
}
```
---
### 3. Toast/Notification Styles
Thêm styles cho toast notifications:
```css
.toast {
background: var(--glass-bg-medium);
backdrop-filter: blur(var(--glass-blur-md));
border: 1px solid var(--glass-border-default);
box-shadow: var(--shadow-lg);
border-radius: var(--radius-lg);
padding: var(--space-4);
}
.toast-success {
border-left: 4px solid var(--accent-success);
}
.toast-error {
border-left: 4px solid var(--accent-error);
}
```
---
## 📝 Checklist Bổ Sung
### High Priority (Nên làm)
- [ ] **Test Light Mode** - Test kỹ light mode trên auth pages
- [ ] **Smooth Scroll** - Thêm smooth scroll behavior
- [ ] **Selection Color** - Custom text selection với X.ai blue
### Medium Priority (Tốt nếu có)
- [ ] **Scrollbar Styling** - Custom scrollbar theo theme
- [ ] **Loading States** - Skeleton loading animation
- [ ] **Focus-Within** - Focus states cho form groups
### Low Priority (Nice to have)
- [ ] **Print Styles** - Print-friendly CSS
- [ ] **Gradient Utilities** - Subtle gradients
- [ ] **Micro-interactions** - Button press animations
- [ ] **Toast Styles** - Notification components
---
## 🎯 Kết Luận
### Tổng Kết
**Theme hiện tại: 98% hoàn chỉnh**
**Điểm mạnh**:
- ✅ X.ai minimal design đã implement đúng 100%
- ✅ Theme variables đầy đủ và nhất quán
- ✅ Glass effects hoàn hảo
- ✅ Accessibility tốt (WCAG 2.1 AA)
- ✅ Dark mode hoàn hảo
**Cần bổ sung** (Optional):
- ⚠️ Test light mode kỹ hơn
- 💡 Smooth scroll behavior
- 💡 Custom text selection
- 💡 Scrollbar styling
- 💡 Loading states
### Khuyến Nghị
**Ưu tiên cao** (15 phút):
1. Test light mode trên auth pages
2. Thêm smooth scroll behavior
3. Thêm custom text selection color
**Ưu tiên trung bình** (30 phút):
4. Custom scrollbar styling
5. Skeleton loading animation
6. Focus-within states
**Ưu tiên thấp** (khi có thời gian):
7. Print styles
8. Gradient utilities
9. Toast/notification styles
### Đánh Giá Cuối
| Tiêu chí | Điểm | Ghi chú |
|----------|------|---------|
| **Completeness** | 9.8/10 | Gần như hoàn hảo |
| **Consistency** | 10/10 | Rất nhất quán |
| **Accessibility** | 10/10 | WCAG 2.1 AA compliant |
| **Performance** | 10/10 | Minimal, fast |
| **Maintainability** | 10/10 | Clean, organized |
**Kết luận**: Theme đã rất tốt, chỉ cần vài improvements nhỏ để đạt 100% hoàn hảo!
---
**Tác giả**: AI Assistant
**Ngày tạo**: 2026-01-05
**Version**: 1.0

View File

@@ -1,370 +0,0 @@
# Theme Improvements - Applied Successfully ✅
**Ngày apply**: 2026-01-05
**Files modified**: 2 files
---
## 📊 Tổng Quan
**🎉 Đã apply thành công 100%** - Tất cả 7 improvements đã được thêm vào theme!
| Improvement | File | Status |
|-------------|------|--------|
| **Smooth Scroll** | globals.css | ✅ Applied |
| **Custom Selection** | globals.css | ✅ Applied |
| **Scrollbar Styling** | globals.css | ✅ Applied |
| **Skeleton Loading** | globals.css | ✅ Applied |
| **Button Press** | globals.css | ✅ Applied |
| **Print Styles** | globals.css | ✅ Applied |
| **Focus-Within** | glass.css | ✅ Applied |
---
## ✅ Chi Tiết Đã Apply
### 1. Smooth Scroll Behavior ✅
**File**: `src/app/globals.css` (Lines 23-48)
**Thêm vào**:
```css
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
/* Disable animations for accessibility */
}
```
**Lợi ích**:
- ✅ Smooth scroll khi click anchor links
- ✅ Respect user preferences (reduced motion)
- ✅ Better UX
---
### 2. Custom Text Selection (X.ai Blue) ✅
**File**: `src/app/globals.css` (Lines 50-62)
**Thêm vào**:
```css
::selection {
background-color: var(--accent-primary);
color: white;
}
::-moz-selection {
background-color: var(--accent-primary);
color: white;
}
```
**Lợi ích**:
- ✅ Brand consistency khi select text
- ✅ X.ai blue highlight
- ✅ Better visual feedback
---
### 3. Custom Scrollbar (X.ai Minimal) ✅
**File**: `src/app/globals.css` (Lines 64-91)
**Thêm vào**:
```css
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border-default);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--glass-border-hover);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--glass-border-default) var(--bg-secondary);
}
```
**Lợi ích**:
- ✅ Consistent với X.ai minimal design
- ✅ Subtle, không gây mất tập trung
- ✅ Cross-browser support (Chrome, Firefox, Safari)
---
### 4. Skeleton Loading Animation ✅
**File**: `src/app/globals.css`
**Thêm animation** (Lines 195-207):
```css
@keyframes skeleton {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
```
**Thêm utilities** (Lines 235-272):
```css
.skeleton {
background: linear-gradient(
90deg,
var(--glass-bg-subtle) 0%,
var(--glass-bg-default) 50%,
var(--glass-bg-subtle) 100%
);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
border-radius: var(--radius-md);
}
.skeleton-text { height: 1em; }
.skeleton-title { height: 2em; }
.skeleton-avatar { width: 48px; height: 48px; }
.skeleton-card { height: 200px; }
```
**Lợi ích**:
- ✅ Better loading UX
- ✅ Consistent với glass theme
- ✅ Ready-to-use utilities
**Usage**:
```tsx
<div className="skeleton skeleton-text" />
<div className="skeleton skeleton-card" />
```
---
### 5. Button Press Micro-interaction ✅
**File**: `src/app/globals.css` (Lines 274-282)
**Thêm vào**:
```css
.btn-press {
transition: transform var(--duration-fast) var(--ease-snap);
}
.btn-press:active:not(:disabled) {
transform: scale(0.98);
}
```
**Lợi ích**:
- ✅ Tactile feedback
- ✅ Better UX
- ✅ Minimal, subtle
**Usage**:
```tsx
<button className="btn-press">Click me</button>
```
---
### 6. Print Styles ✅
**File**: `src/app/globals.css` (Lines 295-337)
**Thêm vào**:
```css
@media print {
body {
background: white;
color: black;
}
.glass-card {
background: white;
border: 1px solid #ccc;
box-shadow: none;
}
/* Hide non-essential elements */
nav, footer, .no-print, button, .auth-controls {
display: none !important;
}
/* Show link URLs */
a[href]:after {
content: " (" attr(href) ")";
}
}
```
**Lợi ích**:
- ✅ Print-friendly pages
- ✅ Hide unnecessary elements
- ✅ Show link URLs
---
### 7. Focus-Within States ✅
**File**: `src/styles/glass.css` (Lines 60-70)
**Thêm vào**:
```css
.glass-card:focus-within {
border-color: var(--accent-primary);
box-shadow: 0 0 0 1px rgba(29, 155, 240, 0.2);
transition: all var(--duration-fast) var(--ease-snap);
}
```
**Lợi ích**:
- ✅ Visual feedback khi focus vào form trong card
- ✅ Better accessibility
- ✅ X.ai blue highlight
---
## 🎯 Kết Quả
### Files Modified
1. **`src/app/globals.css`**:
- ✅ Smooth scroll behavior
- ✅ Custom text selection
- ✅ Custom scrollbar
- ✅ Skeleton loading animation + utilities
- ✅ Button press micro-interaction
- ✅ Print styles
2. **`src/styles/glass.css`**:
- ✅ Focus-within states
### New Utilities Available
```css
/* Skeleton Loading */
.skeleton
.skeleton-text
.skeleton-title
.skeleton-avatar
.skeleton-card
/* Button Press */
.btn-press
```
### Accessibility Improvements
- ✅ Respect `prefers-reduced-motion`
- ✅ Focus-within visual feedback
- ✅ Print-friendly styles
- ✅ Smooth scroll (can be disabled)
### UX Improvements
- ✅ Better loading states (skeleton)
- ✅ Brand consistency (X.ai blue selection)
- ✅ Tactile feedback (button press)
- ✅ Smooth navigation (scroll behavior)
- ✅ Custom scrollbar (minimal design)
---
## 📝 Testing Checklist
### Visual Testing
- [ ] Test smooth scroll với anchor links
- [ ] Select text để xem X.ai blue highlight
- [ ] Scroll page để xem custom scrollbar
- [ ] Test skeleton loading utilities
- [ ] Click buttons để xem press animation
- [ ] Focus vào form trong glass-card
### Accessibility Testing
- [ ] Test với `prefers-reduced-motion` enabled
- [ ] Test keyboard navigation với focus-within
- [ ] Test print preview (Ctrl/Cmd + P)
### Cross-Browser Testing
- [ ] Chrome - Scrollbar styling
- [ ] Firefox - Scrollbar styling
- [ ] Safari - Scrollbar styling
- [ ] All browsers - Selection color
---
## 🎨 Usage Examples
### Skeleton Loading
```tsx
// Loading user profile
<div className="glass-card p-6">
<div className="skeleton skeleton-avatar mb-4" />
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-text" />
<div className="skeleton skeleton-text" />
</div>
// Loading card
<div className="skeleton skeleton-card" />
```
### Button Press
```tsx
<Button className="btn-press" variant="brand">
Click me
</Button>
```
### Print-Friendly
```tsx
// Hide element when printing
<div className="no-print">
<AuthControls />
</div>
```
---
## 📊 Đánh Giá Cuối
| Tiêu chí | Before | After |
|----------|--------|-------|
| **Theme Completeness** | 98% | 100% ✅ |
| **UX Features** | Good | Excellent ✅ |
| **Accessibility** | Good | Excellent ✅ |
| **Loading States** | None | Skeleton ✅ |
| **Print Support** | None | Full ✅ |
**Kết luận**: Theme đã hoàn hảo 100% với tất cả improvements được apply thành công! 🎉
---
**Tác giả**: AI Assistant
**Ngày apply**: 2026-01-05
**Version**: 1.0 (Complete)

View File

@@ -1,197 +0,0 @@
/**
* EN: High Priority Theme Improvements
* VI: Cải tiến theme ưu tiên cao
*
* Add these improvements to globals.css for better UX
* Thêm các cải tiến này vào globals.css để UX tốt hơn
*/
/* ============================================
1. Smooth Scroll Behavior
============================================ */
@layer base {
html {
scroll-behavior: smooth;
}
/* Disable for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
/* Also disable all animations */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}
/* ============================================
2. Custom Text Selection (X.ai Blue)
============================================ */
@layer base {
::selection {
background-color: var(--accent-primary);
color: white;
}
::-moz-selection {
background-color: var(--accent-primary);
color: white;
}
}
/* ============================================
3. Custom Scrollbar (X.ai Minimal)
============================================ */
@layer base {
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border-default);
border-radius: var(--radius-full);
transition: background var(--duration-fast) var(--ease-snap);
}
::-webkit-scrollbar-thumb:hover {
background: var(--glass-border-hover);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--glass-border-default) var(--bg-secondary);
}
}
/* ============================================
4. Skeleton Loading Animation
============================================ */
@keyframes skeleton {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@layer utilities {
.skeleton {
background: linear-gradient(90deg,
var(--glass-bg-subtle) 0%,
var(--glass-bg-default) 50%,
var(--glass-bg-subtle) 100%);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
border-radius: var(--radius-md);
}
/* Skeleton variants */
.skeleton-text {
height: 1em;
margin-bottom: 0.5em;
}
.skeleton-title {
height: 2em;
margin-bottom: 1em;
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
}
.skeleton-card {
height: 200px;
border-radius: var(--radius-lg);
}
}
/* ============================================
5. Focus-Within States for Form Groups
============================================ */
@layer components {
.glass-card:focus-within {
border-color: var(--accent-primary);
box-shadow: 0 0 0 1px rgba(29, 155, 240, 0.2);
transition: all var(--duration-fast) var(--ease-snap);
}
.form-group:focus-within label {
color: var(--accent-primary);
transition: color var(--duration-fast) var(--ease-snap);
}
}
/* ============================================
6. Print Styles
============================================ */
@media print {
body {
background: white;
color: black;
}
.glass-card {
background: white;
border: 1px solid #ccc;
box-shadow: none;
}
/* Hide non-essential elements */
nav,
footer,
.no-print,
button,
.auth-controls {
display: none !important;
}
/* Ensure readable text */
* {
color: black !important;
background: white !important;
}
a {
text-decoration: underline;
}
/* Show link URLs after text */
a[href]:after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #666;
}
}
/* ============================================
7. Button Press Micro-interaction
============================================ */
@layer utilities {
.btn-press {
transition: transform var(--duration-fast) var(--ease-snap);
}
.btn-press:active:not(:disabled) {
transform: scale(0.98);
}
}

View File

@@ -1,597 +0,0 @@
# Moodboard - Visual Direction Guide
> **X.ai Minimal Design Philosophy**
>
> Hướng dẫn visual direction cho GoodGo Web Client theo phong cách Neo-minimalism 2026
## 📋 Overview
**Design Style**: X.ai Minimal / Neo-minimalism 2026
**Target Audience**: Tech-savvy professionals
**Brand Personality**: Sophisticated, Trustworthy, Innovative, Clean
**Last Updated**: 2026-01-04
---
## 🎯 Design Philosophy
### Core Principle: "Less is Enough"
> **Neo-minimalism** is the evolution of minimalism - moving away from cold, sterile designs toward something warmer, more human, and inviting. It's about removing the unnecessary while retaining warmth and personality.
### Key Characteristics
1. **Intentional Simplicity**
- Every element has a purpose
- Remove visual clutter
- White space is a feature, not empty space
2. **Warm Darkness**
- Dark backgrounds that feel inviting, not harsh
- `#15202b` instead of pure black `#000000`
- Reduces eye strain, feels sophisticated
3. **Vibrant Accents**
- Single accent color for focus: X.ai Blue `#1D9BF0`
- High contrast for accessibility
- Blue = trust, technology, reliability
4. **Subtle Depth**
- Minimal shadows, not flat
- Glass effects (glassmorphism) but very subtle
- Layering through transparency, not heavy shadows
5. **Geometric Typography**
- Clean, sans-serif fonts (Inter)
- Strong hierarchy
- Generous line-height for readability
---
## 🎨 Visual Inspiration
### Primary Inspiration: X.ai / Grok
**Why X.ai?**
- Leader in AI interface design
- Perfect balance of minimal + functional
- Warm dark theme with vibrant blue accent
- Clean, distraction-free interfaces
**Key Elements from X.ai**:
- Warm dark gray backgrounds (#15202b family)
- Signature blue accent (#1D9BF0)
- Minimal UI chrome
- Focus on content, not decoration
- Subtle glassmorphism
---
### Secondary Inspirations
#### 1. Linear.app
**What to Learn**:
- Ultra-clean interfaces
- Smooth micro-interactions
- Purple/violet accents on dark backgrounds
- Keyboard-first design
**Visual Elements**:
- Subtle gradients
- Crisp typography
- Minimal shadows
- Fast, snappy animations
---
#### 2. Vercel Dashboard
**What to Learn**:
- Developer-friendly aesthetics
- High contrast for readability
- Clean data visualization
- Monospace typography for code
**Visual Elements**:
- Pure black backgrounds (but we use warmer)
- White/gray text hierarchy
- Accent colors for status
- Edge-to-edge layouts
---
#### 3. Stripe Dashboard
**What to Learn**:
- Trust through design
- Clear visual hierarchy
- Excellent form design
- Error handling patterns
**Visual Elements**:
- Clean form layouts
- Clear labels and instructions
- Inline validation
- Accessible color choices
---
#### 4. Notion
**What to Learn**:
- Content-first approach
- Flexible layouts
- Light + dark themes
- Collaborative feel
**Visual Elements**:
- Generous whitespace
- Subtle hover states
- Friendly, approachable feel
- Emoji as visual elements (optional)
---
## 🌈 Color Direction
### Primary Palette: X.ai Minimal
```
┌─────────────────────────────────────────────────────┐
│ │
│ BACKGROUNDS │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │ │ │
│ │ #15202b │ │ #1a2734 │ │ #1f2f3d │ │ #243442 │ │
│ │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ Primary Secondary Tertiary Elevated │
│ │
│ ACCENT │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │
│ │ #1D9BF0 │ │ #1a8cd8 │ │ #8ecdf7 │ │
│ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ X.ai Blue Hover Light │
│ │
│ TEXT │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ #FFFFFF │ │ #8899A6 │ │ #657786 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ Primary Secondary Tertiary │
│ │
│ STATUS │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ #10B981 │ │ #EF4444 │ │ #F59E0B │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ Success Error Warning │
│ │
└─────────────────────────────────────────────────────┘
```
### Color Psychology
| Color | Hex | Emotion | Usage |
|-------|-----|---------|-------|
| **X.ai Blue** | #1D9BF0 | Trust, Technology, Calm | Primary actions, links, focus |
| **Warm Dark** | #15202b | Sophisticated, Professional | Backgrounds |
| **White** | #FFFFFF | Clean, Clear | Primary text |
| **Green** | #10B981 | Success, Positive | Success states |
| **Red** | #EF4444 | Alert, Important | Errors, destructive |
| **Amber** | #F59E0B | Caution, Attention | Warnings |
---
## 🔤 Typography Direction
### Font Family: Inter
**Why Inter?**
- Designed for screens
- Excellent readability at all sizes
- Geometric but warm
- Open-source, widely available
- Similar to SF Pro (Apple's system font)
### Typography Scale
```
Heading 1: 36px / 2.25rem / font-extrabold / #FFFFFF
"Sign in to your account"
Heading 2: 30px / 1.875rem / font-bold / #FFFFFF
"Section Title"
Heading 3: 24px / 1.5rem / font-semibold / #FFFFFF
"Subsection"
Body: 16px / 1rem / font-normal / #FFFFFF or #8899A6
"Regular paragraph text goes here."
Small: 14px / 0.875rem / font-normal / #8899A6
"Secondary information or labels"
Tiny: 12px / 0.75rem / font-normal / #657786
"Captions, timestamps, footnotes"
```
### Typography Principles
1. **Hierarchy is Key**
- 3 levels max per screen
- Size + Weight + Color = Hierarchy
- Don't rely on color alone
2. **Generous Spacing**
- Line-height: 1.5 for body
- Letter-spacing: -0.02em for headings
- Paragraph spacing: 1.5x line-height
3. **Readability First**
- Max line length: 65-75 characters
- 16px minimum for body (prevents iOS zoom)
- High contrast (WCAG AA minimum)
---
## 🎭 UI Patterns & Elements
### Glassmorphism (Subtle)
**Our Approach**: Ultra-subtle glass effects
```css
/* X.ai Minimal Glass */
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.08);
```
**DO**:
- ✅ Very low opacity (2-5%)
- ✅ Subtle blur (4-12px)
- ✅ Thin, barely visible borders
- ✅ Use sparingly on cards, modals
**DON'T**:
- ❌ Heavy blur (>20px)
- ❌ High opacity (>10%)
- ❌ Thick, obvious borders
- ❌ Glass on glass (double layer)
---
### Shadows
**Our Approach**: Minimal, subtle shadows
```css
/* Light shadow */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
/* Medium shadow */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
/* Glow effect (for focus) */
box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1);
```
**DO**:
- ✅ Soft, diffused shadows
- ✅ Dark shadows on dark backgrounds
- ✅ Blue glow for focus states
- ✅ Minimal elevation differences
**DON'T**:
- ❌ Sharp, hard shadows
- ❌ Light shadows on dark backgrounds
- ❌ Multiple shadow layers
- ❌ Colored shadows (except focus)
---
### Animations & Transitions
**Our Approach**: Fast, snappy, purposeful
```css
/* Fast (hover, focus) */
transition: all 150ms cubic-bezier(0.4, 0.0, 0.2, 1);
/* Normal (page transitions) */
transition: all 300ms cubic-bezier(0.4, 0.0, 0.2, 1);
/* Easing: Material Design "Emphasized" */
cubic-bezier(0.4, 0.0, 0.2, 1)
```
**DO**:
- ✅ Fast transitions (150-300ms)
- ✅ Subtle hover effects (opacity, color change)
- ✅ Smooth easing curves
- ✅ Respect `prefers-reduced-motion`
**DON'T**:
- ❌ Slow animations (>500ms)
- ❌ Bouncy/elastic effects
- ❌ Rotation or complex transforms
- ❌ Animations that block interaction
---
### Buttons
**Primary (X.ai Blue)**:
```
┌─────────────────────────────────────┐
│ │
│ Sign in │
│ │
└─────────────────────────────────────┘
Background: #1D9BF0
Text: #FFFFFF
Height: 48px
Border-radius: 8px
```
**Secondary (Ghost)**:
```
┌─────────────────────────────────────┐
│ │
│ Cancel │
│ │
└─────────────────────────────────────┘
Background: transparent
Text: #8899A6
Border: 1px solid rgba(255,255,255,0.1)
Height: 48px
```
---
### Form Inputs
**Default State**:
```
Email address
┌─────────────────────────────────────┐
│ you@example.com │
└─────────────────────────────────────┘
Background: rgba(255,255,255,0.02)
Border: 1px solid rgba(255,255,255,0.08)
```
**Focus State**:
```
Email address
┌─────────────────────────────────────┐
│ you@example.com | │
└─────────────────────────────────────┘
Border: 1px solid #1D9BF0
Glow: 0 0 0 3px rgba(29,155,240,0.1)
```
---
### Cards
**Glass Card**:
```
┌─────────────────────────────────────────┐
│ │
│ Card Title │
│ │
│ Card content goes here. Keep it │
│ simple and focused on one thing. │
│ │
│ [Action Button] │
│ │
└─────────────────────────────────────────┘
Background: rgba(255,255,255,0.04)
Border: 1px solid rgba(255,255,255,0.08)
Padding: 24-32px
Border-radius: 12px
```
---
## ✅ Do's and Don'ts
### Colors
| ✅ DO | ❌ DON'T |
|-------|----------|
| Use CSS variables (`--accent-primary`) | Hardcode hex values |
| Warm dark gray (#15202b) | Pure black (#000000) |
| Single accent color (X.ai blue) | Multiple bright colors |
| Test contrast ratios (WCAG AA) | Assume colors are accessible |
| Consistent color meanings | Random color choices |
---
### Typography
| ✅ DO | ❌ DON'T |
|-------|----------|
| Use Inter font family | Mix multiple font families |
| Clear hierarchy (3 levels max) | Too many font sizes |
| 16px minimum for body | Small text (<14px for body) |
| High contrast (white on dark) | Low contrast text |
| Generous line-height (1.5) | Cramped text |
---
### Layout & Spacing
| ✅ DO | ❌ DON'T |
|-------|----------|
| 8-point grid system | Random spacing values |
| Generous whitespace | Cramped layouts |
| Single-column forms | Multi-column forms (mobile) |
| Consistent padding | Variable padding |
| Max-width for content (448px forms) | Full-width everything |
---
### Effects & Decorations
| ✅ DO | ❌ DON'T |
|-------|----------|
| Subtle glass effects (2-5% opacity) | Heavy glassmorphism (>10%) |
| Minimal shadows | Multiple shadow layers |
| Fast transitions (150-300ms) | Slow animations (>500ms) |
| Blue glow for focus | Rainbow/gradient glows |
| ~~Cosmic backgrounds~~ Solid backgrounds | Complex background patterns |
---
### Components
| ✅ DO | ❌ DON'T |
|-------|----------|
| Touch-friendly sizes (44px+) | Small touch targets |
| Clear hover/focus states | Missing interaction states |
| Loading states | No feedback during actions |
| Error messages with icons | Error messages with color only |
| Disabled states clearly visible | Subtle disabled states |
---
## 🖼️ Visual Reference Board
### Approved Styles ✅
**Authentication Pages**:
- Centered form layout
- Glass card container
- X.ai blue primary button
- Warm dark background (#15202b)
- Subtle icon container with blue tint
- No cosmic/floating background effects
**Form Elements**:
- Top-aligned labels
- Subtle input backgrounds
- Blue focus rings
- Inline validation messages
- Password visibility toggle
**Buttons**:
- Solid X.ai blue (not gradient)
- Subtle hover darkening
- Loading spinner inline
- Full-width on mobile
---
### Rejected Styles ❌
**Avoid**:
- Pure black backgrounds (#000000)
- White accent colors (low visibility)
- Floating blur orbs/cosmic effects
- Gradient buttons
- Heavy shadows
- Bouncy animations
- Decorative illustrations (for auth)
- Multiple accent colors
---
## 📱 Responsive Considerations
### Mobile-First Approach
1. **Start with mobile** (375px)
2. **Enhance for tablet** (768px)
3. **Optimize for desktop** (1280px+)
### Key Mobile Adjustments
- Full-width containers
- Larger touch targets (48px)
- Reduced spacing (75% of desktop)
- Stacked layouts
- Simplified navigation
- 16px font for inputs (prevent zoom)
---
## 🧪 Design Validation Checklist
Before finalizing any design:
**Visual**:
- [ ] Uses approved color palette
- [ ] Typography follows scale
- [ ] Spacing follows 8-point grid
- [ ] Glass effects are subtle
- [ ] Shadows are minimal
- [ ] Animations are fast (<300ms)
**Accessibility**:
- [ ] Contrast ratio ≥ 4.5:1
- [ ] Focus states visible
- [ ] Touch targets ≥ 44px
- [ ] Error messages clear
- [ ] Color not sole indicator
**Consistency**:
- [ ] Matches existing components
- [ ] Uses design tokens (CSS variables)
- [ ] Follows established patterns
- [ ] Works in dark + light themes
---
## 🔗 Resources & References
### Design Tools
- [Figma](https://figma.com) - Primary design tool
- [Storybook](http://localhost:6006) - Component library
### Inspiration Sources
- [X.ai](https://x.ai) - Primary inspiration
- [Linear.app](https://linear.app) - UI patterns
- [Vercel](https://vercel.com) - Developer aesthetics
- [Stripe](https://stripe.com) - Form patterns
### Guidelines
- [X.ai Brand Guidelines](https://x.ai/legal/brand-guidelines)
- [Material Design 3](https://m3.material.io)
- [Apple Human Interface Guidelines](https://developer.apple.com/design/)
### Learning
- [Refactoring UI](https://refactoringui.com)
- [UI Design Daily](https://uidesigndaily.com)
- [Mobbin](https://mobbin.com) - UI patterns library
---
## 📝 Version History
### v2.0.0 (2026-01-04) - X.ai Minimal Redesign
- Adopted X.ai blue accent (#1D9BF0)
- Changed from pure black to warm dark (#15202b)
- Removed cosmic background effects
- Simplified glassmorphism
- Updated animation guidelines
### v1.0.0 (2025-12-01) - Initial Design
- White accent on black background
- Cosmic floating effects
- Gradient buttons
---
## 📚 Related Documentation
- [Design System](../DESIGN_SYSTEM.md)
- [Login Mockups](./mockups/auth-login-states.md)
- [Mobile Specs](./responsive/mobile-specs.md)
- [Implementation Guide](../implementation/auth-pages-implementation.md)
---
**Created by**: Design Team
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal)
**Status**: ✅ Approved & Active

View File

@@ -1,447 +0,0 @@
# Login Page - UI Mockups & States
> **High-Fidelity Design Specifications**
>
> Tất cả states của trang đăng nhập theo X.ai minimal design
## 📋 Overview
**Page**: Login (`/login`)
**Figma**: [Add Figma link here]
**Last Updated**: 2026-01-04
---
## 🎨 Design States
### 1. Default State (Initial Load)
**Description**: Trạng thái ban đầu khi user vào trang
#### Visual Specs
**Background**:
- Color: `#15202b` (warm dark gray)
- Pattern: Solid, no cosmic effects
**Form Container**:
- Width: `448px` (max-w-md)
- Padding: `32px` (p-8)
- Background: `rgba(255, 255, 255, 0.04)` (glass-card)
- Border: `1px solid rgba(255, 255, 255, 0.08)`
- Border-radius: `12px` (rounded-xl)
- Backdrop-filter: `blur(8px)`
- Shadow: `0 2px 8px rgba(0, 0, 0, 0.3)`
**Logo/Icon Container**:
- Size: `56px x 56px`
- Padding: `12px` (p-3)
- Background: `rgba(29, 155, 240, 0.05)` (X.ai blue 5%)
- Border: `1px solid rgba(29, 155, 240, 0.1)`
- Border-radius: `16px` (rounded-2xl)
- Icon: Star (white, 40x40px)
**Heading**:
- Text: "Sign in to your account"
- Font: Inter, 36px (text-4xl)
- Weight: 800 (font-extrabold)
- Color: `#FFFFFF`
- Line-height: `1.25` (tracking-tight)
**Subheading**:
- Text: "Enter your credentials to access your account"
- Font: Inter, 14px (text-sm)
- Color: `#8899A6` (text-secondary)
**Form Fields**:
**Email Input**:
- Label: "Email address"
- Placeholder: "you@example.com"
- Background: `rgba(255, 255, 255, 0.02)`
- Border: `1px solid rgba(255, 255, 255, 0.08)`
- Height: `44px`
- Border-radius: `8px`
- Font: 16px
- Color: `#FFFFFF`
**Password Input**:
- Label: "Password"
- Placeholder: "••••••••"
- Same styling as email
- Eye icon: `24x24px`, color `#8899A6`
**Remember Me Checkbox**:
- Size: `20x20px`
- Border: `1px solid rgba(255, 255, 255, 0.2)`
- Border-radius: `4px`
- Label: "Remember me", 14px, `#8899A6`
**Forgot Password Link**:
- Text: "Forgot password?"
- Font: 14px (text-sm)
- Color: `#1D9BF0` (X.ai blue)
- Hover: `#1a8cd8`
**Sign In Button**:
- Text: "Sign in"
- Width: `100%`
- Height: `48px`
- Background: `#1D9BF0` (X.ai blue)
- Color: `#FFFFFF`
- Border-radius: `8px`
- Font: 16px, weight 600
- Shadow: `0 2px 8px rgba(0, 0, 0, 0.1)`
**Sign Up Link**:
- Text: "Don't have an account? Sign up"
- Font: 14px
- "Sign up" part: `#1D9BF0`
**Theme/Language Controls** (top-right):
- Position: `fixed top-6 right-6`
- Gap: `12px`
- Each button: `40x40px`, glass effect
#### Screenshot
![Default State](../../../public/design/mockups/login-default.png)
*TODO: Add screenshot*
---
### 2. Hover States
#### Email Input Hover
**Changes**:
- Border: `1px solid rgba(255, 255, 255, 0.12)`
- Transition: `150ms ease`
#### Password Input Hover
**Changes**: Same as email
#### Sign In Button Hover
**Changes**:
- Background: `#1a8cd8` (darker blue)
- Shadow: `0 4px 12px rgba(0, 0, 0, 0.15)`
- Transition: `150ms ease`
#### Link Hover
**Changes**:
- Color: `#1a8cd8`
- Transition: `150ms ease`
#### Screenshot
![Hover States](../../../public/design/mockups/login-hover.png)
*TODO: Add screenshot*
---
### 3. Focus States
#### Email Input Focus
**Changes**:
- Border: `1px solid #1D9BF0` (X.ai blue)
- Background: `rgba(255, 255, 255, 0.04)`
- Ring: `2px solid rgba(29, 155, 240, 0.3)`
- Ring offset: `2px`
- Outline: `none`
#### Password Input Focus
**Changes**: Same as email
#### Button Focus
**Changes**:
- Ring: `2px solid rgba(29, 155, 240, 0.3)`
- Ring offset: `2px`
#### Screenshot
![Focus States](../../../public/design/mockups/login-focus.png)
*TODO: Add screenshot with focused input*
---
### 4. Active/Typing State
#### Email Input with Text
**Changes**:
- Value: "user@example.com"
- Text color: `#FFFFFF`
- Cursor: blinking white line
#### Password Input with Text
**Changes**:
- Value: "••••••••" (masked)
- Eye icon clickable
#### Screenshot
![Typing State](../../../public/design/mockups/login-typing.png)
*TODO: Add screenshot*
---
### 5. Validation Error State
#### Email Error
**Trigger**: Invalid email format
**Visual Changes**:
- Border: `1px solid #EF4444` (error red)
- Background: `rgba(239, 68, 68, 0.1)`
- Error message below:
- Text: "Please enter a valid email address"
- Color: `#EF4444`
- Icon: Alert circle (16x16px)
- Font: 14px (text-sm)
- Margin-top: `4px`
#### Password Error
**Trigger**: Password < 8 characters
**Visual Changes**:
- Border: `1px solid #EF4444`
- Background: `rgba(239, 68, 68, 0.1)`
- Error message: "Password must be at least 8 characters"
#### Form-level Error
**Trigger**: API returns error (invalid credentials)
**Visual Changes**:
- Alert box above form:
- Background: `rgba(239, 68, 68, 0.1)`
- Border: `1px solid rgba(239, 68, 68, 0.2)`
- Border-radius: `8px`
- Padding: `12px 16px`
- Icon: X circle (20x20px, red)
- Text: "Invalid email or password. Please try again."
- Color: `#EF4444`
- Font: 14px
#### Screenshot
![Error States](../../../public/design/mockups/login-error.png)
*TODO: Add screenshot*
---
### 6. Loading State
#### Button Loading
**Trigger**: Form submitted, waiting for API
**Visual Changes**:
- Button disabled
- Text: "Signing in..."
- Spinner:
- Size: `20x20px`
- Color: `#FFFFFF`
- Position: Left of text
- Animation: Spin (1s linear infinite)
- Opacity: `0.7`
- Cursor: `not-allowed`
**Form Fields**:
- All inputs disabled
- Opacity: `0.5`
#### Screenshot
![Loading State](../../../public/design/mockups/login-loading.png)
*TODO: Add screenshot*
---
### 7. Success State (Redirect)
**Note**: Very brief state before redirect to dashboard
**Visual Changes**:
- Success toast (optional):
- Background: `rgba(16, 185, 129, 0.1)`
- Border: `1px solid rgba(16, 185, 129, 0.2)`
- Icon: Checkmark circle (green)
- Text: "Login successful! Redirecting..."
- Position: Top-center
- Duration: 1s before redirect
#### Screenshot
![Success State](../../../public/design/mockups/login-success.png)
*TODO: Add screenshot*
---
### 8. Disabled State
**Trigger**: Network error or maintenance mode
**Visual Changes**:
- All inputs disabled
- Button disabled:
- Background: `rgba(29, 155, 240, 0.3)`
- Cursor: `not-allowed`
- Opacity: `0.5`
- Message: "Login is temporarily unavailable"
#### Screenshot
![Disabled State](../../../public/design/mockups/login-disabled.png)
*TODO: Add screenshot*
---
## 📱 Responsive Variations
### Mobile (< 640px)
**Changes**:
- Container: Full width with `16px` padding
- Form: Full width
- Stack vertically
- Touch targets: Min `44x44px`
- Font sizes may adjust slightly
**Screenshot**:
![Mobile View](../../../public/design/mockups/login-mobile.png)
*TODO: Add screenshot*
---
### Tablet (640px - 1024px)
**Changes**:
- Container: Centered, same max-width (448px)
- Similar to desktop
**Screenshot**:
![Tablet View](../../../public/design/mockups/login-tablet.png)
*TODO: Add screenshot*
---
### Desktop (> 1024px)
**Changes**: Default state as described above
**Screenshot**:
![Desktop View](../../../public/design/mockups/login-desktop.png)
*TODO: Add screenshot*
---
## 🎨 Dark vs Light Theme
### Dark Theme (Default)
- Background: `#15202b`
- Text: `#FFFFFF`
- As described above
### Light Theme
**Changes**:
- Background: `#FFFFFF`
- Text: `#1D1D1F` (dark)
- Glass effect: `rgba(0, 0, 0, 0.04)`
- Borders: `rgba(0, 0, 0, 0.1)`
- X.ai blue stays: `#1D9BF0`
**Screenshot**:
![Light Theme](../../../public/design/mockups/login-light.png)
*TODO: Add screenshot*
---
## 🔤 Typography Specs
| Element | Font | Size | Weight | Color | Line Height |
|---------|------|------|--------|-------|-------------|
| H1 Heading | Inter | 36px | 800 | #FFFFFF | 1.25 |
| Subheading | Inter | 14px | 400 | #8899A6 | 1.5 |
| Input Label | Inter | 14px | 500 | #8899A6 | 1.5 |
| Input Text | Inter | 16px | 400 | #FFFFFF | 1.5 |
| Button Text | Inter | 16px | 600 | #FFFFFF | 1.5 |
| Link Text | Inter | 14px | 500 | #1D9BF0 | 1.5 |
| Error Text | Inter | 14px | 400 | #EF4444 | 1.5 |
---
## 🎨 Color Palette
| Purpose | Color | Hex | Usage |
|---------|-------|-----|-------|
| Background | Warm Dark Gray | #15202b | Main background |
| X.ai Blue | Primary Accent | #1D9BF0 | Buttons, links, focus |
| X.ai Blue Hover | Darker Blue | #1a8cd8 | Hover states |
| White | Primary Text | #FFFFFF | Headings, input text |
| Light Gray | Secondary Text | #8899A6 | Labels, descriptions |
| Red | Error | #EF4444 | Error messages, borders |
| Green | Success | #10B981 | Success messages |
---
## 📐 Spacing & Layout
**Form Container**:
- Max-width: `448px`
- Padding: `32px`
- Gap between elements: `24px`
**Form Fields**:
- Label to input: `8px`
- Input to input: `16px`
- Input height: `44px`
**Buttons**:
- Height: `48px`
- Margin-top: `24px`
**Mobile**:
- Side padding: `16px`
- All gaps reduced by 25%
---
## ♿ Accessibility Notes
**Contrast Ratios**:
- White on #15202b: 12.8:1 (WCAG AAA ✅)
- X.ai blue on #15202b: 5.2:1 (WCAG AA ✅)
- Error red on white bg: 4.5:1 (WCAG AA ✅)
**Focus Indicators**:
- All interactive elements have visible focus ring
- X.ai blue ring with 30% opacity
- 2px thick, 2px offset
**Keyboard Navigation**:
- Tab order: Email → Password → Remember me → Button → Forgot password → Sign up link
- Enter key submits form
**Screen Reader**:
- Form has proper labels
- Error messages announced with `role="alert"`
- Loading state announced
---
## 🧪 Design Validation Checklist
- [ ] All states designed (8 states)
- [ ] Responsive variants (mobile, tablet, desktop)
- [ ] Dark + Light theme variants
- [ ] Typography specs documented
- [ ] Color palette defined
- [ ] Spacing consistent with 8-point grid
- [ ] Accessibility requirements met
- [ ] Screenshots added to Figma/public folder
- [ ] Interactive prototype created
---
## 🔗 Related Documentation
- [Login User Flow](../../ux/flows/auth-login.md)
- [Implementation Guide](../../implementation/auth-pages-implementation.md)
- [Design System](../../DESIGN_SYSTEM.md)
- [Responsive Specs](../responsive/mobile-specs.md)
---
**Designer**: [Name]
**Reviewer**: [Name]
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal Redesign)

View File

@@ -1,554 +0,0 @@
# Mobile Responsive Design Specifications
> **Mobile-First Design Guide**
>
> Breakpoint: `< 640px` (sm in Tailwind)
## 📱 Overview
**Target Devices**:
- iPhone 12/13/14: 390px width
- iPhone SE: 375px width
- Android phones: 360px - 414px width
**Orientation**: Portrait (primary), Landscape (secondary)
**Last Updated**: 2026-01-04
---
## 🎯 Mobile Design Principles
### 1. Touch-First Interaction
- Minimum touch target: `44x44px` (Apple HIG)
- Recommended: `48x48px` (Material Design)
- Spacing between touchable elements: `8px` minimum
### 2. Thumb Zone Optimization
- Primary actions: Bottom center (easy reach)
- Secondary actions: Top corners
- Avoid: Middle edges (hard to reach)
### 3. Content Priority
- Show essential content only
- Progressive disclosure for details
- Reduce visual clutter
### 4. Performance
- Optimize images for mobile
- Minimize animations
- Fast load times (< 2s)
---
## 📋 Auth Pages - Mobile Specifications
### Login Page (`/login`)
#### Layout
**Container**:
```css
width: 100vw;
min-height: 100vh;
padding: 16px; /* Reduced from 32px */
background: #15202b;
```
**Form Container**:
```css
width: 100%;
max-width: 100%; /* Remove 448px limit */
padding: 24px; /* Reduced from 32px */
margin: auto;
```
**Vertical Spacing**:
- Logo to heading: `16px` (reduced from 24px)
- Heading to form: `20px` (reduced from 24px)
- Between inputs: `12px` (reduced from 16px)
- Form to button: `20px` (reduced from 24px)
#### Typography
**Heading**:
```css
font-size: 28px; /* Reduced from 36px */
line-height: 1.2;
margin-bottom: 8px;
```
**Subheading**:
```css
font-size: 14px; /* Same */
line-height: 1.4;
```
**Input Labels**:
```css
font-size: 14px;
margin-bottom: 6px;
```
**Input Text**:
```css
font-size: 16px; /* Must be 16px to prevent zoom on iOS */
```
#### Form Elements
**Input Fields**:
```css
height: 48px; /* Increased from 44px for better touch */
padding: 12px 16px;
border-radius: 8px;
font-size: 16px; /* Prevent iOS zoom */
```
**Submit Button**:
```css
height: 52px; /* Increased from 48px */
width: 100%;
font-size: 16px;
border-radius: 8px;
```
**Links**:
```css
min-height: 44px; /* Touch target */
padding: 8px 0;
display: inline-block;
```
**Checkbox**:
```css
width: 24px; /* Increased from 20px */
height: 24px;
margin-right: 8px;
```
#### Theme/Language Controls
**Position**:
```css
position: fixed;
top: 16px; /* Reduced from 24px */
right: 16px;
z-index: 50;
```
**Size**:
```css
width: 40px;
height: 40px;
gap: 8px;
```
#### Screenshot Layout
![Mobile Login](../../../public/design/mockups/mobile-login-layout.png)
*TODO: Add annotated screenshot*
---
### Register Page (`/register`)
**Same as Login with additions**:
#### Password Strength Indicator
**Bars**:
```css
height: 4px; /* Reduced from 6px */
width: 100%;
gap: 4px;
border-radius: 2px;
```
**Text**:
```css
font-size: 12px;
margin-top: 4px;
```
#### Terms Checkbox
**Hit Area**:
```css
min-height: 44px;
padding: 8px 0;
```
**Text**:
```css
font-size: 13px; /* Slightly smaller */
line-height: 1.5;
```
#### Screenshot
![Mobile Register](../../../public/design/mockups/mobile-register-layout.png)
*TODO: Add screenshot*
---
### Forgot Password Page (`/forgot-password`)
**Same mobile specs as Login**
#### Success State
**Message Box**:
```css
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
```
#### Screenshot
![Mobile Forgot Password](../../../public/design/mockups/mobile-forgot-password.png)
*TODO: Add screenshot*
---
## 📐 Grid & Spacing
### 8-Point Grid (Mobile Adjusted)
**Base unit**: 4px
**Common spacing**:
- Extra small: `4px` (spacing-1)
- Small: `8px` (spacing-2)
- Medium: `12px` (spacing-3)
- Default: `16px` (spacing-4)
- Large: `20px` (spacing-5)
- Extra large: `24px` (spacing-6)
**Container Padding**:
- Side padding: `16px` (px-4)
- Top/bottom: `16px` (py-4)
---
## 🎨 Typography Scale (Mobile)
| Element | Size | Weight | Line Height | Letter Spacing |
|---------|------|--------|-------------|----------------|
| H1 | 28px | 800 | 1.2 | -0.02em |
| H2 | 24px | 700 | 1.25 | -0.01em |
| H3 | 20px | 600 | 1.3 | 0 |
| Body | 16px | 400 | 1.5 | 0 |
| Small | 14px | 400 | 1.5 | 0 |
| Tiny | 12px | 400 | 1.4 | 0 |
**Important**: All input fields MUST use `font-size: 16px` to prevent iOS auto-zoom.
---
## 🔘 Interactive Elements
### Touch Targets
**Minimum Sizes**:
- Buttons: `48x48px` or `100% width x 52px`
- Links: `44px` min-height
- Checkboxes: `24x24px`
- Radio buttons: `24x24px`
- Toggle switches: `52x32px`
**Spacing**:
- Between buttons: `12px`
- Button to text: `8px`
- Icon to text: `8px`
### Button Variations
**Primary Button (Sign In)**:
```css
width: 100%;
height: 52px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
```
**Secondary Button** (if needed):
```css
width: 100%;
height: 48px;
font-size: 15px;
font-weight: 500;
```
**Link Button**:
```css
min-height: 44px;
padding: 10px 0;
display: inline-block;
```
---
## ⌨️ Virtual Keyboard Handling
### iOS Safari
**Issue**: Keyboard covers bottom of viewport
**Solutions**:
1. **Input Focus**:
```javascript
// Scroll input into view
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
```
2. **Form Position**:
```css
/* Keep submit button visible */
.form-container {
margin-bottom: 300px; /* Space for keyboard */
}
```
3. **Viewport Units**:
```css
/* Avoid 100vh on mobile */
min-height: 100dvh; /* Dynamic viewport height */
```
### Android
**Input Types**:
```html
<!-- Show correct keyboard -->
<input type="email" inputmode="email" />
<input type="tel" inputmode="tel" />
<input type="number" inputmode="numeric" />
```
---
## 🖼️ Images & Assets
### Icon Sizes (Mobile)
| Element | Size | Format |
|---------|------|--------|
| Logo | 40x40px | SVG |
| Form icons | 20x20px | SVG |
| Success/Error icons | 20x20px | SVG |
| Menu icons | 24x24px | SVG |
### Image Export
**Density**:
- Standard: @1x
- Retina: @2x
- Super Retina: @3x
**Format**:
- Icons: SVG (preferred)
- Photos: WebP or JPEG
- Transparency: PNG
---
## 🎭 Animations (Mobile)
### Reduced Motion
**Respect User Preference**:
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
### Performance
**Avoid**:
- Heavy blur effects (< 8px ok)
- Too many simultaneous animations
- Animating `width`, `height`, `top`, `left`
**Use**:
- `transform` (translateY, scale)
- `opacity`
- Hardware acceleration: `will-change: transform`
### Transitions
**Fast**:
```css
transition: all 150ms cubic-bezier(0.4, 0.0, 0.2, 1);
```
**Normal**:
```css
transition: all 300ms cubic-bezier(0.4, 0.0, 0.2, 1);
```
---
## 📊 Performance Targets (Mobile)
### Load Times
- First Contentful Paint: < 1.5s
- Time to Interactive: < 2.5s
- Total page weight: < 500KB
### Lighthouse Scores
- Performance: ≥ 85
- Accessibility: ≥ 95
- Best Practices: ≥ 90
---
## ♿ Mobile Accessibility
### Touch Targets
- Minimum: `44x44px`
- Ideal: `48x48px`
- Spacing: `8px` between targets
### Text Sizing
- All text scalable (no `font-size: 16px !important`)
- Test with iOS text size settings
- Test with Android display size settings
### Screen Readers
- iOS VoiceOver compatible
- Android TalkBack compatible
- Proper heading hierarchy
- Form labels associated
### Focus Management
- Visible focus indicators
- Focus not trapped unintentionally
- Logical tab order
---
## 🧪 Testing Checklist
### Devices to Test
**iOS**:
- [ ] iPhone 14 Pro (393x852)
- [ ] iPhone 14 (390x844)
- [ ] iPhone SE (375x667)
**Android**:
- [ ] Samsung Galaxy S21 (360x800)
- [ ] Google Pixel 7 (412x915)
### Orientations
- [ ] Portrait (primary)
- [ ] Landscape (secondary)
### Actions
- [ ] Form filling with virtual keyboard
- [ ] Submit form
- [ ] Navigate between fields (Tab)
- [ ] Tap all interactive elements
- [ ] Zoom in/out (pinch)
- [ ] Scroll form
- [ ] Theme toggle
- [ ] Language switcher
### Edge Cases
- [ ] Long email addresses
- [ ] Small screen (320px width)
- [ ] Large text size (iOS settings)
- [ ] Slow network (3G simulation)
- [ ] Offline mode
---
## 🐛 Common Mobile Issues & Solutions
### Issue 1: Input Zoom on iOS
**Problem**: Safari zooms in when focusing input < 16px
**Solution**: Always use `font-size: 16px` for inputs
### Issue 2: Keyboard Covers Submit Button
**Problem**: Virtual keyboard hides button
**Solution**: Add bottom margin or use `scrollIntoView()`
### Issue 3: Touch Targets Too Small
**Problem**: Users miss buttons
**Solution**: Minimum `44x44px`, ideally `48x48px`
### Issue 4: Viewport Height Issues
**Problem**: `100vh` includes browser chrome
**Solution**: Use `100dvh` (dynamic viewport height)
### Issue 5: Click Delay
**Problem**: 300ms delay on iOS
**Solution**: Add `<meta name="viewport" content="width=device-width">`
---
## 🎨 Mobile-Specific Patterns
### Form Layout
**Stack Vertically**:
```
[Logo]
[Heading]
[Subheading]
[Email Label]
[Email Input]
[Password Label]
[Password Input]
[Remember Me] [Forgot Password]
[Submit Button]
[Sign Up Link]
```
### Error Messages
**Position**: Below field (not inline)
**Icon**: Left of text
**Color**: Red with light background
**Dismissible**: Tap X to close
### Loading States
**Full Screen Overlay** (for page transitions):
```css
position: fixed;
inset: 0;
background: rgba(21, 32, 43, 0.8);
backdrop-filter: blur(4px);
z-index: 9999;
```
**Inline** (for button):
- Show spinner in button
- Disable form
- Keep button in place (no layout shift)
---
## 🔗 Related Documentation
- [Tablet Specs](./tablet-specs.md)
- [Desktop Specs](./desktop-specs.md)
- [Design System](../../DESIGN_SYSTEM.md)
- [Auth Mockups](../mockups/auth-login-states.md)
---
**Designer**: [Name]
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal Redesign)

View File

@@ -1,393 +0,0 @@
# User Flow: Login
> **Goal**: Người dùng đăng nhập thành công vào hệ thống
## 📋 Flow Overview
**Entry Points**:
- Direct URL: `/login`
- From landing page
- After logout
- After session expiry
- From protected route redirect
**Exit Points**:
- Success → Dashboard (`/`)
- Forgot Password → `/forgot-password`
- Sign Up → `/register`
---
## 🗺️ Flow Diagram
```
┌─────────────┐
│ Entry Point │
└──────┬──────┘
v
┌──────────────────────────────┐
│ Login Page Loads │
│ - Email input (empty) │
│ - Password input (empty) │
│ - Remember me (unchecked) │
│ - Theme/Language controls │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ User Enters Email │
│ - Validation on blur │
│ - Error if invalid format │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ User Enters Password │
│ - Show/hide toggle │
│ - Validation on blur │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ User Clicks "Sign In" │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ Client-side Validation │
│ - Check all fields filled │
│ - Check valid email format │
└──────┬───────────────────────┘
├─ Invalid → Show errors inline
v Valid
┌──────────────────────────────┐
│ Show Loading State │
│ - Disable button │
│ - Show spinner │
└──────┬───────────────────────┘
v
┌──────────────────────────────┐
│ Send API Request │
│ POST /api/auth/login │
└──────┬───────────────────────┘
├─────────────────┬──────────────────┐
│ │ │
v Success v Invalid Creds v Server Error
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Save Token │ │ Show Error: │ │ Show Error: │
│ Set Cookie │ │ "Invalid email │ │ "Something went │
│ Update Auth │ │ or password" │ │ wrong" │
│ State │ │ - Clear password │ │ - Keep form data │
└──────┬──────┘ │ - Focus email │ │ - Enable retry │
│ └──────────────────┘ └──────────────────┘
v
┌─────────────┐
│ Redirect to │
│ Dashboard │
└─────────────┘
```
---
## 📝 Detailed Steps
### Step 1: Page Load
**Action**: User lands on `/login`
**UI State**:
- Clean form with empty fields
- "Sign In" button enabled
- Theme toggle (top-right)
- Language switcher (top-right)
**Technical**:
- Check if already authenticated → redirect to `/`
- Load theme preference from localStorage
- Load language preference from localStorage
---
### Step 2: Email Input
**Action**: User types email address
**Validation** (on blur):
- ✅ Required field
- ✅ Valid email format (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`)
**Error Messages**:
- Empty: "Email is required"
- Invalid: "Please enter a valid email address"
**UI Behavior**:
- Focus ring: X.ai blue (#1D9BF0)
- Error: Red border + error text below
---
### Step 3: Password Input
**Action**: User types password
**Features**:
- Password visibility toggle (eye icon)
- Masked by default
**Validation** (on blur):
- ✅ Required field
- ✅ Minimum length (8 characters)
**Error Messages**:
- Empty: "Password is required"
- Too short: "Password must be at least 8 characters"
---
### Step 4: Remember Me (Optional)
**Action**: User checks "Remember me"
**Behavior**:
- If checked: Session persists 30 days
- If unchecked: Session expires on browser close
---
### Step 5: Form Submission
**Action**: User clicks "Sign In" button
**Client-side Validation**:
```typescript
// Validation rules
{
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
required: true,
minLength: 8
}
}
```
**If Invalid**:
- Show all errors
- Focus first invalid field
- Don't send API request
**If Valid**:
- Proceed to API call
---
### Step 6: Loading State
**UI Changes**:
- Button shows spinner
- Button text: "Signing in..."
- Button disabled
- Form fields disabled
**Duration**: Until API responds (typically 500ms-2s)
---
### Step 7: API Response Handling
#### ✅ Success (200 OK)
**API Response**:
```json
{
"success": true,
"data": {
"token": "jwt_token_here",
"user": {
"id": "123",
"email": "user@example.com",
"name": "John Doe"
}
}
}
```
**Actions**:
1. Save token to localStorage/cookie
2. Update auth state (Zustand)
3. Show success toast (optional)
4. Redirect to `/` (dashboard)
---
#### ❌ Invalid Credentials (401 Unauthorized)
**API Response**:
```json
{
"success": false,
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Invalid email or password"
}
}
```
**Actions**:
1. Show error message below form
2. Clear password field
3. Focus email field
4. Keep email value
5. Enable retry
**Error Display**:
```
⚠️ Invalid email or password. Please try again.
```
---
#### ⚠️ Account Locked (403 Forbidden)
**API Response**:
```json
{
"success": false,
"error": {
"code": "ACCOUNT_LOCKED",
"message": "Too many failed attempts. Try again in 15 minutes."
}
}
```
**Actions**:
1. Show specific error
2. Disable form for 15 minutes (optional)
3. Show countdown timer
---
#### 💥 Server Error (500)
**API Response**:
```json
{
"success": false,
"error": {
"code": "SERVER_ERROR",
"message": "Something went wrong. Please try again."
}
}
```
**Actions**:
1. Show generic error message
2. Keep form data
3. Enable retry button
---
## 🔀 Alternative Paths
### Path A: Forgot Password
**Trigger**: User clicks "Forgot password?" link
**Action**: Navigate to `/forgot-password`
---
### Path B: Sign Up
**Trigger**: User clicks "Don't have an account? Sign up" link
**Action**: Navigate to `/register`
---
### Path C: Social Login (Future)
**Trigger**: User clicks "Continue with Google" button
**Action**:
1. Open OAuth popup
2. Handle callback
3. Same success flow as email/password
---
## ✅ Success Criteria
Flow is successful when:
- ✅ User is authenticated
- ✅ Token is stored
- ✅ User is redirected to dashboard
- ✅ Dashboard loads with user data
---
## 🧪 Testing Checklist
**Functional Tests**:
- [ ] Can submit with valid credentials
- [ ] Cannot submit with empty fields
- [ ] Cannot submit with invalid email
- [ ] Error shown for wrong password
- [ ] Loading state appears during API call
- [ ] Remember me persists session
- [ ] Forgot password link works
- [ ] Sign up link works
**Edge Cases**:
- [ ] Already authenticated user redirected
- [ ] Network error handled gracefully
- [ ] API timeout handled (> 10s)
- [ ] Special characters in password work
- [ ] Copy-paste email works
- [ ] Browser autofill works
**Accessibility**:
- [ ] Keyboard navigation works (Tab, Enter)
- [ ] Screen reader announces errors
- [ ] Focus management correct
- [ ] Error messages have role="alert"
**Performance**:
- [ ] Page loads < 1s
- [ ] API response < 2s
- [ ] No layout shift during loading
---
## 📱 Responsive Behavior
**Mobile (< 640px)**:
- Stack form vertically
- Full-width button
- Touch-friendly input size (min 44px height)
- Virtual keyboard doesn't hide submit button
**Tablet (640-1024px)**:
- Same as mobile, centered
**Desktop (> 1024px)**:
- Centered form (max-width: 448px)
- Ample padding around form
---
## 🎨 Figma Prototype
[Add link to Figma prototype here]
---
## 📊 Analytics Events
Track these events:
- `login_page_viewed`
- `login_form_submitted`
- `login_success`
- `login_failed` (with error code)
- `forgot_password_clicked`
- `sign_up_clicked`
---
**Maintained by**: UX Team
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal Redesign)

View File

@@ -1,109 +0,0 @@
# GoodGo Web Client - Sitemap
> **Information Architecture** - Cấu trúc tổng quan của ứng dụng
>
> Last Updated: 2026-01-04
## 🗺️ Application Structure
```
GoodGo Web Client
├── 🔐 Authentication (Unauthenticated)
│ ├── /login - Login Page
│ │ ├── Email input
│ │ ├── Password input
│ │ ├── Remember me checkbox
│ │ ├── Forgot password link → /forgot-password
│ │ └── Sign up link → /register
│ │
│ ├── /register - Registration Page
│ │ ├── Email input
│ │ ├── Password input (with strength indicator)
│ │ ├── Confirm password input
│ │ ├── Terms & conditions checkbox
│ │ └── Sign in link → /login
│ │
│ └── /forgot-password - Password Reset
│ ├── Email input
│ ├── Success state (email sent)
│ └── Back to login link → /login
├── 🏠 Dashboard (Authenticated) - /
│ ├── Overview section
│ ├── Analytics widgets
│ ├── Quick actions
│ └── Navigation to other sections
├── 👤 Profile - /profile
│ ├── User information
│ ├── Settings
│ │ ├── Theme toggle (Light/Dark)
│ │ ├── Language switcher (EN/VI)
│ │ └── Other preferences
│ └── Account security
└── [Add more sections as they're developed]
```
## 📱 Mobile Navigation
Mobile navigation should include:
- Hamburger menu icon (top-left or top-right)
- Collapsible sidebar
- Bottom navigation bar (optional, for key actions)
## 🎨 Visual Hierarchy
1. **Level 1**: Authentication pages (public)
2. **Level 2**: Main dashboard (authenticated)
3. **Level 3**: Feature-specific pages
4. **Level 4**: Settings & profile
## 🔗 External Links
- Help Center (future)
- Privacy Policy (future)
- Terms of Service (future)
## 📝 Navigation Patterns
### Primary Navigation
- Logo (top-left) → Dashboard
- Main menu items (horizontal or sidebar)
- User menu (top-right)
- Profile
- Settings
- Logout
### Secondary Navigation
- Breadcrumbs (for deep pages)
- Tabs (for section switching)
- Back buttons (for flows)
## 🚀 Future Additions
Document new pages/sections here as they're added:
- [ ] [Feature Name] - /route
- [ ] [Feature Name] - /route
## 🔍 SEO & Meta
Each page should have:
- Unique title tag
- Meta description
- Open Graph tags (for social sharing)
## 📊 Analytics Tracking
Pages to track:
- All auth pages (login, register, forgot-password)
- Dashboard landing
- Conversion funnels (signup → onboarding)
---
**Maintained by**: Design Team
**Review frequency**: Monthly or when major features added

View File

@@ -1,610 +0,0 @@
# Authentication Pages - Wireframes
> **UX Wireframe Documentation**
>
> Low và Mid-fidelity wireframes cho 3 trang xác thực
## 📋 Overview
**Pages**: Login, Register, Forgot Password
**Purpose**: Visual structure trước khi design chi tiết
**Tools**: Figma, Sketch, or hand-drawn
**Last Updated**: 2026-01-04
---
## 🎯 Wireframe Levels
### Low-Fidelity (Lo-Fi)
**Purpose**: Validate layout và information hierarchy
**Details**: Boxes, lines, placeholder text
**Colors**: Grayscale only (no brand colors)
**Time**: Quick sketches (15-30 mins per page)
### Mid-Fidelity (Mid-Fi)
**Purpose**: Define spacing, sizing, content
**Details**: Actual text, proper sizing, grid system
**Colors**: Still grayscale, but more refined
**Time**: Detailed mockups (1-2 hours per page)
---
## 📄 Page 1: Login
### Low-Fidelity Wireframe
```
┌─────────────────────────────────────┐
│ │ Theme/Lang
│ [App Icon/Logo] │ ◐ EN▼
│ │
│ Sign In │
│ Subtitle text here │
│ │
│ Email address │
│ ┌─────────────────────────────┐ │
│ │ your@email.com │ │
│ └─────────────────────────────┘ │
│ │
│ Password │
│ ┌─────────────────────────────┐ │
│ │ •••••••• 👁 │ │
│ └─────────────────────────────┘ │
│ │
│ ☐ Remember me Forgot password│
│ │
│ ┌─────────────────────────────┐ │
│ │ Sign In │ │
│ └─────────────────────────────┘ │
│ │
│ Don't have an account? Sign up │
│ │
└─────────────────────────────────────┘
Annotations:
- Container: 448px max-width, centered
- Vertical spacing: 24px between sections
- Input height: 44px
- Button height: 48px
```
**Figma Link**: [Add lo-fi wireframe link]
**Screenshot**:
![Login Lo-Fi](../../../public/design/wireframes/login-lofi.png)
*TODO: Add wireframe screenshot*
---
### Mid-Fidelity Wireframe
```
┌────────────────────────────────────────────┐
│ │ [Theme] [Lang]
│ ┌────────┐ │
│ │ [★] │ App Icon │
│ └────────┘ 56x56px │
│ │
│ Sign in to your account │ 36px, Bold
│ Enter your credentials to access... │ 14px, Gray
│ │
│ │
│ Email address │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ you@example.com │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ │ 16px gap
│ Password │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ •••••••• 👁 │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ │ 12px gap
│ ☐ Remember me Forgot password?│ 14px
│ │
│ │ 24px gap
│ ┌──────────────────────────────────┐ │
│ │ Sign in │ │ 16px, 600 weight
│ └──────────────────────────────────┘ │ 48px height
│ │
│ │ 16px gap
│ Don't have an account? Sign up │ 14px
│ ──────────────────── ─────── │ Blue underline
│ │
└────────────────────────────────────────────┘
Measurements:
- Container: max-width 448px, padding 32px
- Form to edges: 32px padding
- Label to input: 8px
- Input to input: 16px
- Checkbox to link: space-between
- Button margin-top: 24px
Grid:
- 8-point grid system
- Vertical rhythm: 8px base
Typography:
- H1: 36px, extrabold
- Subtitle: 14px, regular
- Labels: 14px, medium
- Inputs: 16px, regular
- Button: 16px, semibold
- Links: 14px, medium
```
**Figma Link**: [Add mid-fi wireframe link]
**Screenshot**:
![Login Mid-Fi](../../../public/design/wireframes/login-midfi.png)
*TODO: Add wireframe screenshot*
---
## 📄 Page 2: Register
### Low-Fidelity Wireframe
```
┌─────────────────────────────────────┐
│ │ Theme/Lang
│ [App Icon/Logo] │ ◐ EN▼
│ │
│ Create Account │
│ Subtitle text here │
│ │
│ Email address │
│ ┌─────────────────────────────┐ │
│ │ your@email.com │ │
│ └─────────────────────────────┘ │
│ │
│ Password │
│ ┌─────────────────────────────┐ │
│ │ •••••••• 👁 │ │
│ └─────────────────────────────┘ │
│ Strength: [▓▓▓▓░] Strong │
│ │
│ Confirm Password │
│ ┌─────────────────────────────┐ │
│ │ •••••••• 👁 │ │
│ └─────────────────────────────┘ │
│ │
│ ☐ I agree to Terms & Conditions │
│ │
│ ┌─────────────────────────────┐ │
│ │ Sign Up │ │
│ └─────────────────────────────┘ │
│ │
│ Already have an account? Sign in│
│ │
└─────────────────────────────────────┘
Annotations:
- Password strength: 4-bar indicator
- Confirm password: validates match
- Terms checkbox: required
- Same spacing as login
```
**Figma Link**: [Add lo-fi wireframe link]
**Screenshot**:
![Register Lo-Fi](../../../public/design/wireframes/register-lofi.png)
*TODO: Add wireframe screenshot*
---
### Mid-Fidelity Wireframe
```
┌────────────────────────────────────────────┐
│ │ [Theme] [Lang]
│ ┌────────┐ │
│ │ [★] │ App Icon │
│ └────────┘ 56x56px │
│ │
│ Create your account │ 36px, Bold
│ Get started with your free account │ 14px, Gray
│ │
│ │
│ Email address │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ you@example.com │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ │ 16px gap
│ Password │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ •••••••• 👁 │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ Password strength │ 12px gray
│ ┌──┬──┬──┬──┐ │ 4px height bars
│ │▓▓│▓▓│▓▓│░░│ Strong │ Green text
│ └──┴──┴──┴──┘ │
│ │ 12px gap
│ Confirm Password │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ •••••••• 👁 │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ │ 16px gap
│ ☐ I agree to Terms of Service and │ 14px
│ Privacy Policy │ Blue links
│ │
│ │ 24px gap
│ ┌──────────────────────────────────┐ │
│ │ Create Account │ │ 16px, 600 weight
│ └──────────────────────────────────┘ │ 48px height
│ │
│ │ 16px gap
│ Already have an account? Sign in │ 14px
│ ────────────────────────── ─────── │ Blue underline
│ │
└────────────────────────────────────────────┘
Password Strength States:
- Weak: 1 bar (Red)
- Fair: 2 bars (Orange)
- Good: 3 bars (Amber)
- Strong: 4 bars (Green)
Validation:
- Email: valid format
- Password: min 8 chars, 1 uppercase, 1 number
- Confirm: matches password
- Terms: must be checked
```
**Figma Link**: [Add mid-fi wireframe link]
**Screenshot**:
![Register Mid-Fi](../../../public/design/wireframes/register-midfi.png)
*TODO: Add wireframe screenshot*
---
## 📄 Page 3: Forgot Password
### Low-Fidelity Wireframe
```
┌─────────────────────────────────────┐
│ │ Theme/Lang
│ [App Icon/Logo] │ ◐ EN▼
│ │
│ Reset Password │
│ Subtitle text here │
│ │
│ Email address │
│ ┌─────────────────────────────┐ │
│ │ your@email.com │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Send Reset Link │ │
│ └─────────────────────────────┘ │
│ │
│ ← Back to Sign in │
│ │
└─────────────────────────────────────┘
Success State:
┌─────────────────────────────────────┐
│ [✓ Icon] │
│ │
│ Check your email │
│ We sent reset link to... │
│ │
│ [Open Email App] │
│ │
│ Send to another email │
│ ← Back to Sign in │
└─────────────────────────────────────┘
```
**Figma Link**: [Add lo-fi wireframe link]
**Screenshot**:
![Forgot Password Lo-Fi](../../../public/design/wireframes/forgot-password-lofi.png)
*TODO: Add wireframe screenshot*
---
### Mid-Fidelity Wireframe
**Initial State**:
```
┌────────────────────────────────────────────┐
│ │ [Theme] [Lang]
│ ┌────────┐ │
│ │ [★] │ App Icon │
│ └────────┘ 56x56px │
│ │
│ Reset your password │ 36px, Bold
│ Enter your email to receive a... │ 14px, Gray
│ │
│ │
│ Email address │ 14px label
│ ┌──────────────────────────────────┐ │
│ │ you@example.com │ │ 16px text
│ └──────────────────────────────────┘ │ 44px height
│ │
│ │ 24px gap
│ ┌──────────────────────────────────┐ │
│ │ Send Reset Link │ │ 16px, 600 weight
│ └──────────────────────────────────┘ │ 48px height
│ │
│ │ 16px gap
│ ← Back to Sign in │ 14px, blue
│ │
└────────────────────────────────────────────┘
```
**Success State**:
```
┌────────────────────────────────────────────┐
│ │ [Theme] [Lang]
│ │
│ ┌────────┐ │
│ │ [✓] │ Success Icon │
│ └────────┘ 56x56px, Green │
│ │
│ Check your email │ 36px, Bold
│ We've sent a password reset link │ 14px, Gray
│ to your@example.com │
│ │
│ │ 24px gap
│ ┌──────────────────────────────────┐ │
│ │ Open Email App │ │ 16px, Blue button
│ └──────────────────────────────────┘ │ 48px height
│ │
│ │ 12px gap
│ Didn't receive email? Resend │ 14px
│ │
│ │ 16px gap
│ ← Back to Sign in │ 14px, blue
│ │
└────────────────────────────────────────────┘
Notes:
- Success icon: Green checkmark circle
- Email shown: User's entered email
- Open Email App: Deep link to mail client
- Resend link: Available after 60 seconds
```
**Figma Link**: [Add mid-fi wireframe link]
**Screenshot**:
![Forgot Password Mid-Fi](../../../public/design/wireframes/forgot-password-midfi.png)
*TODO: Add wireframe screenshot*
---
## 📐 Design Grid & Spacing
### 8-Point Grid System
**Base Unit**: 8px
**Vertical Rhythm**:
- Extra small: 4px (0.5 unit)
- Small: 8px (1 unit)
- Medium: 16px (2 units)
- Large: 24px (3 units)
- Extra large: 32px (4 units)
- Section: 48px (6 units)
**Horizontal Spacing**:
- Container padding: 32px (4 units)
- Mobile padding: 16px (2 units)
- Element spacing: 8px, 12px, 16px, 24px
### Layout Grid
**Desktop (> 1024px)**:
- Max-width: 448px
- Centered in viewport
- Padding: 32px
**Tablet (640-1024px)**:
- Max-width: 448px
- Centered
- Padding: 24px
**Mobile (< 640px)**:
- Full width
- Padding: 16px
- Adjust spacing (reduce by 25%)
---
## 🎨 Wireframe Conventions
### Visual Elements
**Boxes**:
- Rectangle: Input field, button, container
- Rounded rectangle: Button (8px radius)
- Circle: Icon, avatar
- Line: Separator
**Text**:
- UPPERCASE: Labels, headings
- Mixed case: Body text, placeholder
- Lorem ipsum: Placeholder content
- Actual copy: Mid-fi onwards
**Icons**:
- [X]: Close/remove
- [?]: Help/info
- [⚙]: Settings
- [★]: App icon placeholder
- [👁]: Show/hide password
- [✓]: Success, checkmark
- [⚠]: Warning
- [ⓘ]: Information
**Interaction States**:
- Dotted line: Focus state
- Gray background: Disabled state
- Bold outline: Active/selected state
---
## 📱 Responsive Wireframe Variants
### Mobile Wireframe (< 640px)
**Changes from Desktop**:
- Full-width layout
- Reduced padding (16px)
- Smaller heading (28px)
- Adjusted spacing (reduce 25%)
- Stack all elements vertically
- Theme/Lang controls: Smaller (36px)
**Screenshot**:
![Mobile Wireframe](../../../public/design/wireframes/auth-mobile-wireframe.png)
*TODO: Add mobile wireframe*
---
### Tablet Wireframe (640-1024px)
**Changes from Desktop**:
- Same max-width (448px)
- Centered layout
- Slightly adjusted padding (24px)
---
## 🔄 User Flow Integration
### Wireframe Flow Diagram
```
┌──────────┐
│ Login │────────────┐
│ Page │ │
└────┬─────┘ │
│ ▼
│ ┌───────────┐
│ │ Forgot │
│ │ Password │
│ └───────────┘
┌──────────┐
│ Register │
│ Page │
└──────────┘
Transitions:
- Login → Forgot: "Forgot password?" link
- Login → Register: "Sign up" link
- Forgot → Login: "Back to sign in" link
- Register → Login: "Sign in" link
```
### Page States
**Login**:
1. Default (empty form)
2. Typing (with values)
3. Error (validation failed)
4. Loading (submitting)
5. Success (redirect)
**Register**:
1. Default (empty form)
2. Typing (with password strength)
3. Error (validation failed)
4. Loading (submitting)
5. Success (redirect or confirm email)
**Forgot Password**:
1. Default (empty form)
2. Typing (email entered)
3. Loading (sending)
4. Success (email sent confirmation)
5. Error (email not found)
---
## ✅ Wireframe Review Checklist
### Content
- [ ] All necessary fields included
- [ ] Labels clear and descriptive
- [ ] CTAs (Call-to-Actions) obvious
- [ ] Error states considered
- [ ] Success states designed
- [ ] Loading states indicated
### Layout
- [ ] Visual hierarchy clear
- [ ] Important elements prominent
- [ ] Spacing consistent
- [ ] Alignment proper
- [ ] Grid system applied
- [ ] Responsive variants created
### Interaction
- [ ] Touch targets adequate (44px+)
- [ ] Links distinguishable
- [ ] Form flow logical
- [ ] Error messages positioned well
- [ ] Focus states indicated
### Accessibility
- [ ] Heading hierarchy correct (H1, H2, H3)
- [ ] Form labels associated with inputs
- [ ] Focus order logical
- [ ] Color not sole indicator
- [ ] Alt text for images/icons (noted)
---
## 🔗 Next Steps
After wireframes approved:
1. **Create High-Fidelity Mockups**:
- Apply X.ai color palette
- Add glassmorphism effects
- Finalize typography
- Design all states
- See: [Auth Mockups](../mockups/auth-login-states.md)
2. **Build Interactive Prototype**:
- Link pages in Figma
- Add transitions
- Test user flow
- Get stakeholder feedback
3. **Handoff to Development**:
- Export assets
- Create specs
- Write implementation guide
- See: [Implementation Guide](../../implementation/auth-pages-implementation.md)
---
## 📚 Related Documentation
- [Login User Flow](../flows/auth-login.md)
- [Sitemap](../sitemap.md)
- [High-Fi Mockups](../../ui/mockups/auth-login-states.md)
- [Mobile Specs](../../ui/responsive/mobile-specs.md)
- [Design System](../../DESIGN_SYSTEM.md)
---
**Created by**: UX Team
**Last Updated**: 2026-01-04
**Version**: 2.0 (X.ai Minimal Redesign)
**Status**: Ready for High-Fi Design

View File

@@ -1,59 +0,0 @@
'use client';
import React from 'react';
import { cn } from '@/shared/lib/utils';
import { BrandLogo } from '@/features/shared/components/brand/brand-logo';
interface AuthCardProps {
children: React.ReactNode;
title: string;
description?: string;
className?: string;
footer?: React.ReactNode;
}
/**
* EN: Shared component for consistent Auth page layouts
* VI: Component dùng chung cho layout nhất quán giữa các trang Auth
*/
export function AuthCard({
children,
title,
description,
className,
footer
}: AuthCardProps) {
return (
<div className="flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md md:max-w-[400px] space-y-8 flex flex-col items-center">
{/* EN: Logo & Header / VI: Logo & Tiêu đề */}
<div className="flex flex-col items-center">
<BrandLogo variant="icon" size="lg" className="mb-6" />
<h1 className="text-3xl font-bold text-text-primary tracking-tight">
{title}
</h1>
{description && (
<p className="mt-2 text-sm text-text-secondary">
{description}
</p>
)}
</div>
{/* EN: Main Content / VI: Nội dung chính */}
<div className={cn(
'w-full glass-card p-8 rounded-3xl shadow-glass-lg border border-glass-medium',
'animate-in fade-in zoom-in-95 duration-normal',
className
)}>
{children}
{footer && (
<div className="text-center text-sm text-text-secondary mt-8">
{footer}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,21 +0,0 @@
/**
* EN: Authentication Feature Public API
* VI: Public API của Feature Authentication
*
* This file exports the public API of the authentication feature.
* File này export public API của feature authentication.
*/
// Components
// export * from './components/login-form';
// export * from './components/register-form';
// export * from './components/forgot-password-form';
// Hooks
// export * from './hooks/use-auth';
// Stores
// export { useAuthStore } from './stores/auth.store';
// Types
// export type * from './types/auth.types';

View File

@@ -1,6 +0,0 @@
/**
* EN: Auth feature library exports
* VI: Xuất thư viện của feature auth
*/
export * from './oauth';

View File

@@ -1,113 +0,0 @@
/**
* EN: OAuth utility functions for social authentication
* VI: Các hàm tiện ích OAuth cho xác thực mạng xã hội
*/
/**
* EN: Get API base URL from environment
* VI: Lấy API base URL từ environment
*/
const getApiBaseUrl = (): string => {
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1';
};
/**
* EN: OAuth provider types
* VI: Các loại provider OAuth
*/
export type OAuthProvider = 'google' | 'facebook' | 'github';
/**
* EN: Initiate OAuth flow by redirecting to backend OAuth endpoint
* VI: Bắt đầu luồng OAuth bằng cách chuyển hướng đến endpoint OAuth của backend
*
* @param provider - OAuth provider (google, facebook, github)
* @param redirectUrl - Optional redirect URL after successful authentication
*/
export const initiateOAuth = (provider: OAuthProvider, redirectUrl?: string): void => {
// EN: Store redirect URL in sessionStorage for use after callback
// VI: Lưu redirect URL vào sessionStorage để sử dụng sau callback
if (redirectUrl && typeof window !== 'undefined') {
sessionStorage.setItem('oauth_redirect', redirectUrl);
}
// EN: Build OAuth initiation URL
// VI: Xây dựng URL bắt đầu OAuth
const apiBaseUrl = getApiBaseUrl();
const oauthUrl = `${apiBaseUrl}/auth/social/${provider}`;
// EN: Redirect to backend OAuth endpoint
// VI: Chuyển hướng đến endpoint OAuth của backend
if (typeof window !== 'undefined') {
window.location.href = oauthUrl;
}
};
/**
* EN: Handle OAuth callback by extracting token from URL
* VI: Xử lý OAuth callback bằng cách trích xuất token từ URL
*
* @param token - Access token from OAuth callback
* @returns Access token string
*/
export const handleOAuthCallback = (token: string): string => {
return token;
};
/**
* EN: Handle OAuth error by extracting error message from URL
* VI: Xử lý lỗi OAuth bằng cách trích xuất thông báo lỗi từ URL
*
* @param errorMessage - Error message from OAuth callback
* @returns Error message string
*/
export const handleOAuthError = (errorMessage: string): string => {
return decodeURIComponent(errorMessage);
};
/**
* EN: Get stored redirect URL from sessionStorage
* VI: Lấy redirect URL đã lưu từ sessionStorage
*
* @returns Redirect URL or null
*/
export const getStoredRedirectUrl = (): string | null => {
if (typeof window === 'undefined') {
return null;
}
const redirectUrl = sessionStorage.getItem('oauth_redirect');
if (redirectUrl) {
sessionStorage.removeItem('oauth_redirect');
}
return redirectUrl;
};
/**
* EN: Google OAuth initiation helper
* VI: Helper để bắt đầu OAuth Google
*
* @param redirectUrl - Optional redirect URL after successful authentication
*/
export const signInWithGoogle = (redirectUrl?: string): void => {
initiateOAuth('google', redirectUrl);
};
/**
* EN: Facebook OAuth initiation helper
* VI: Helper để bắt đầu OAuth Facebook
*
* @param redirectUrl - Optional redirect URL after successful authentication
*/
export const signInWithFacebook = (redirectUrl?: string): void => {
initiateOAuth('facebook', redirectUrl);
};
/**
* EN: GitHub OAuth initiation helper
* VI: Helper để bắt đầu OAuth GitHub
*
* @param redirectUrl - Optional redirect URL after successful authentication
*/
export const signInWithGitHub = (redirectUrl?: string): void => {
initiateOAuth('github', redirectUrl);
};

View File

@@ -1,300 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { Button } from '@/features/shared/components/ui/button';
import { useTranslations } from 'next-intl';
/**
* EN: ChatInput component props interface
* VI: Interface cho props của component ChatInput
*/
export interface ChatInputProps {
/**
* EN: Current input value / VI: Giá trị input hiện tại
*/
value?: string;
/**
* EN: Callback when input value changes / VI: Callback khi giá trị input thay đổi
*/
onChange?: (value: string) => void;
/**
* EN: Callback when send button is clicked or Enter is pressed / VI: Callback khi nút send được click hoặc Enter được nhấn
*/
onSend?: (message: string) => void;
/**
* EN: Callback when attach file button is clicked / VI: Callback khi nút attach file được click
*/
onAttachFile?: () => void;
/**
* EN: Placeholder text / VI: Text placeholder
*/
placeholder?: string;
/**
* EN: Disabled state / VI: Trạng thái vô hiệu hóa
*/
disabled?: boolean;
/**
* EN: Loading state - shows spinner on send button / VI: Trạng thái loading - hiển thị spinner trên nút send
*/
loading?: boolean;
/**
* EN: Maximum height for textarea before scrolling / VI: Chiều cao tối đa cho textarea trước khi scroll
*/
maxHeight?: number;
/**
* EN: Minimum height for textarea / VI: Chiều cao tối thiểu cho textarea
*/
minHeight?: number;
/**
* EN: Additional CSS classes / VI: Các class CSS bổ sung
*/
className?: string;
}
/**
* EN: ChatInput component - Textarea input with auto-resize, attach file button, and send button
* VI: Component ChatInput - Textarea input với auto-resize, nút attach file và nút send
*
* Features:
* - Auto-resize textarea based on content
* - Keyboard shortcuts: Enter to send, Shift+Enter for new line
* - Attach file button (optional)
* - Send button with loading state
* - Follows design system theme colors
*
* Tính năng:
* - Textarea tự động thay đổi kích thước dựa trên nội dung
* - Phím tắt: Enter để gửi, Shift+Enter để xuống dòng
* - Nút attach file (tùy chọn)
* - Nút send với trạng thái loading
* - Tuân theo màu sắc của design system
*
* @example
* ```tsx
* <ChatInput
* value={message}
* onChange={setMessage}
* onSend={handleSend}
* onAttachFile={handleAttachFile}
* placeholder="Type your message..."
* />
* ```
*/
export function ChatInput({
value: externalValue,
onChange,
onSend,
onAttachFile,
placeholder,
disabled = false,
loading = false,
maxHeight = 200,
minHeight = 44,
className,
}: ChatInputProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const defaultPlaceholder = placeholder || t('chat.typeMessage');
// EN: Reference to textarea element for auto-resize / VI: Reference đến element textarea cho auto-resize
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
// EN: Internal state for uncontrolled mode / VI: State nội bộ cho chế độ uncontrolled
const [internalValue, setInternalValue] = React.useState('');
// EN: Use external value if provided, otherwise use internal state / VI: Sử dụng external value nếu có, nếu không dùng internal state
const value = externalValue !== undefined ? externalValue : internalValue;
// EN: Handle textarea input change / VI: Xử lý thay đổi input textarea
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
// EN: Update internal state if uncontrolled / VI: Cập nhật internal state nếu uncontrolled
if (externalValue === undefined) {
setInternalValue(newValue);
}
onChange?.(newValue);
// EN: Auto-resize textarea / VI: Tự động thay đổi kích thước textarea
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
const scrollHeight = textareaRef.current.scrollHeight;
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textareaRef.current.style.height = `${newHeight}px`;
}
};
// EN: Handle keyboard shortcuts / VI: Xử lý phím tắt
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// EN: Enter to send (unless Shift is pressed for new line) / VI: Enter để gửi (trừ khi nhấn Shift để xuống dòng)
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// EN: Handle send message / VI: Xử lý gửi tin nhắn
const handleSend = () => {
const trimmedValue = value.trim();
if (trimmedValue && !disabled && !loading && onSend) {
onSend(trimmedValue);
// EN: Reset textarea height after sending / VI: Đặt lại chiều cao textarea sau khi gửi
if (textareaRef.current) {
textareaRef.current.style.height = `${minHeight}px`;
}
// EN: Clear internal state if uncontrolled / VI: Xóa internal state nếu uncontrolled
if (externalValue === undefined) {
setInternalValue('');
}
// EN: Clear input value (expecting parent to handle this via onChange) / VI: Xóa giá trị input (kỳ vọng parent xử lý qua onChange)
onChange?.('');
}
};
// EN: Adjust textarea height on mount and when value changes / VI: Điều chỉnh chiều cao textarea khi mount và khi value thay đổi
React.useEffect(() => {
if (textareaRef.current) {
// EN: Reset height to auto to get accurate scrollHeight / VI: Đặt lại chiều cao về auto để lấy scrollHeight chính xác
textareaRef.current.style.height = 'auto';
const scrollHeight = textareaRef.current.scrollHeight;
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textareaRef.current.style.height = `${newHeight}px`;
}
}, [value, minHeight, maxHeight]);
// EN: Check if send button should be enabled / VI: Kiểm tra xem nút send có nên được kích hoạt không
const canSend = value.trim().length > 0 && !disabled && !loading;
return (
<div
className={cn(
'flex items-end gap-2 w-full',
'border-t border-border-primary bg-bg-secondary',
'p-4',
// EN: Mobile: Sticky bottom / VI: Mobile: Sticky bottom
'max-md:sticky max-md:bottom-0 max-md:z-10',
// EN: Mobile: Safe area padding for iOS / VI: Mobile: Safe area padding cho iOS
'max-md:pb-[env(safe-area-inset-bottom)]',
className
)}
>
{/* EN: Attach file button / VI: Nút attach file */}
{onAttachFile && (
<button
type="button"
onClick={onAttachFile}
disabled={disabled}
className={cn(
'flex-shrink-0',
// EN: Mobile: Minimum 44px touch target / VI: Mobile: Touch target tối thiểu 44px
'w-10 h-10 md:w-10 md:h-10',
'min-w-[44px] min-h-[44px]',
'flex items-center justify-center',
'rounded-md',
'text-text-tertiary hover:text-text-secondary',
'hover:bg-bg-tertiary active:bg-bg-elevated',
'transition-colors duration-[150ms]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-primary focus-visible:ring-offset-2',
disabled && 'opacity-50 cursor-not-allowed'
)}
aria-label={t('chat.attachFile')}
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.122 2.122l7.81-7.81"
/>
</svg>
</button>
)}
{/* EN: Textarea input / VI: Textarea input */}
<div className="flex-1 relative">
<textarea
ref={textareaRef}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={disabled}
placeholder={defaultPlaceholder}
rows={1}
className={cn(
'w-full',
'px-4 py-3',
'bg-bg-tertiary',
'border border-border-primary rounded-lg',
'text-text-primary placeholder:text-text-tertiary',
'resize-none',
'overflow-y-auto',
'focus-visible:outline-none',
'focus-visible:border-accent-primary',
'focus-visible:ring-2 focus-visible:ring-accent-primary focus-visible:ring-offset-2',
'focus-visible:shadow-[0_0_20px_rgba(59,130,246,0.3)]',
'transition-all duration-[150ms]',
'text-base',
disabled && 'cursor-not-allowed opacity-50',
// EN: Custom scrollbar styling / VI: Styling scrollbar tùy chỉnh
'scrollbar-thin scrollbar-thumb-border-secondary scrollbar-track-transparent',
'hover:scrollbar-thumb-border-secondary'
)}
style={{
minHeight: `${minHeight}px`,
maxHeight: `${maxHeight}px`,
}}
aria-label={t('chat.messageInput')}
aria-describedby="chat-input-help"
/>
{/* EN: Hidden helper text for screen readers / VI: Text hướng dẫn ẩn cho screen readers */}
<span id="chat-input-help" className="sr-only">
{t('chat.sendHelp')}
</span>
</div>
{/* EN: Send button / VI: Nút send */}
<Button
type="button"
onClick={handleSend}
isDisabled={!canSend}
isLoading={loading}
variant="primary"
size="md"
className={cn(
'flex-shrink-0',
'w-10 h-10 p-0',
// EN: Mobile: Minimum 44px touch target / VI: Mobile: Touch target tối thiểu 44px
'min-w-[44px] min-h-[44px]',
'rounded-lg',
// EN: Adjust button styles for square shape / VI: Điều chỉnh styles cho button hình vuông
!canSend && 'opacity-50 cursor-not-allowed'
)}
aria-label={t('chat.send')}
>
{!loading && (
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
)}
</Button>
</div>
);
}

View File

@@ -1,304 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { Menu, X } from 'lucide-react';
import { useTranslations } from 'next-intl';
/**
* EN: Chat layout component props interface
* VI: Interface cho props của component Chat layout
*/
export interface ChatLayoutProps {
/**
* EN: Left sidebar content (conversation list, search, etc.)
* VI: Nội dung sidebar trái (danh sách cuộc trò chuyện, tìm kiếm, etc.)
*/
sidebar?: React.ReactNode;
/**
* EN: Main chat area content (messages, header, input)
* VI: Nội dung khu vực chat chính (tin nhắn, header, input)
*/
children: React.ReactNode;
/**
* EN: Right panel content (settings, participants, etc.) - optional
* VI: Nội dung panel bên phải (cài đặt, người tham gia, etc.) - tùy chọn
*/
rightPanel?: React.ReactNode;
/**
* EN: Whether the sidebar is visible (for mobile responsiveness)
* VI: Sidebar có hiển thị hay không (cho responsive mobile)
*/
sidebarVisible?: boolean;
/**
* EN: Whether the right panel is visible
* VI: Panel bên phải có hiển thị hay không
*/
rightPanelVisible?: boolean;
/**
* EN: Callback when sidebar visibility changes
* VI: Callback khi trạng thái hiển thị sidebar thay đổi
*/
onSidebarToggle?: (visible: boolean) => void;
/**
* EN: Callback when right panel visibility changes
* VI: Callback khi trạng thái hiển thị panel bên phải thay đổi
*/
onRightPanelToggle?: (visible: boolean) => void;
/**
* EN: Additional CSS classes
* VI: Các class CSS bổ sung
*/
className?: string;
}
/**
* EN: Chat layout component - Main layout structure for chat interface
* VI: Component Chat layout - Cấu trúc layout chính cho giao diện chat
*
* Layout structure:
* - Left Sidebar (280px): Conversation list, search, user profile
* - Main Chat Area (flex-1, max 768px centered): Messages, header, input
* - Right Panel (320px, optional): Settings, participants, shared files
*
* Responsive behavior:
* - Mobile (< 768px): Hide sidebar by default, full-width messages
* - Tablet (768px - 1024px): Sidebar + Main (two columns)
* - Desktop (> 1024px): Sidebar + Main + Right Panel (three columns)
*
* Cấu trúc layout:
* - Sidebar trái (280px): Danh sách cuộc trò chuyện, tìm kiếm, profile người dùng
* - Khu vực chat chính (flex-1, tối đa 768px căn giữa): Tin nhắn, header, input
* - Panel bên phải (320px, tùy chọn): Cài đặt, người tham gia, file đã chia sẻ
*
* Hành vi responsive:
* - Mobile (< 768px): Ẩn sidebar mặc định, tin nhắn full-width
* - Tablet (768px - 1024px): Sidebar + Main (hai cột)
* - Desktop (> 1024px): Sidebar + Main + Right Panel (ba cột)
*
* @example
* ```tsx
* <ChatLayout
* sidebar={<ConversationSidebar />}
* rightPanel={<ConversationSettings />}
* >
* <ChatMessages />
* </ChatLayout>
* ```
*/
export function ChatLayout({
sidebar,
children,
rightPanel,
sidebarVisible = true,
rightPanelVisible = false,
onSidebarToggle,
onRightPanelToggle: _onRightPanelToggle,
className,
}: ChatLayoutProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Mobile: Hide sidebar by default / VI: Mobile: Ẩn sidebar mặc định
const [mobileSidebarVisible, setMobileSidebarVisible] = React.useState(false);
const [touchStart, setTouchStart] = React.useState<number | null>(null);
const [touchEnd, setTouchEnd] = React.useState<number | null>(null);
// EN: Minimum swipe distance (px) / VI: Khoảng cách swipe tối thiểu (px)
const minSwipeDistance = 50;
// EN: Handle touch start / VI: Xử lý bắt đầu chạm
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
};
// EN: Handle touch move / VI: Xử lý di chuyển chạm
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
// EN: Handle touch end / VI: Xử lý kết thúc chạm
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
// EN: Swipe right to open sidebar / VI: Vuốt phải để mở sidebar
if (isRightSwipe && !mobileSidebarVisible) {
setMobileSidebarVisible(true);
onSidebarToggle?.(true);
}
// EN: Swipe left to close sidebar / VI: Vuốt trái để đóng sidebar
if (isLeftSwipe && mobileSidebarVisible) {
setMobileSidebarVisible(false);
onSidebarToggle?.(false);
}
};
// EN: Sync mobile sidebar state with prop / VI: Đồng bộ state sidebar mobile với prop
React.useEffect(() => {
if (typeof window !== 'undefined' && window.innerWidth < 768) {
setMobileSidebarVisible(sidebarVisible);
}
}, [sidebarVisible]);
// EN: Tablet: Show sidebar by default, but toggleable / VI: Tablet: Hiện sidebar mặc định, nhưng có thể toggle
const [tabletSidebarVisible, setTabletSidebarVisible] = React.useState(true);
// EN: Determine if sidebar should be visible / VI: Xác định sidebar có nên hiển thị không
const getSidebarVisibility = () => {
if (typeof window === 'undefined') return sidebarVisible;
const width = window.innerWidth;
if (width < 768) {
// EN: Mobile: Use mobile state / VI: Mobile: Dùng state mobile
return mobileSidebarVisible;
} else if (width >= 768 && width < 1024) {
// EN: Tablet: Use tablet state / VI: Tablet: Dùng state tablet
return tabletSidebarVisible;
}
// EN: Desktop: Use prop / VI: Desktop: Dùng prop
return sidebarVisible;
};
const isSidebarVisible = getSidebarVisibility();
return (
<div
className={cn(
// EN: Base layout container / VI: Container layout cơ bản
'flex h-screen w-full overflow-hidden bg-bg-primary',
className
)}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{/* EN: Mobile/Tablet menu button / VI: Nút menu mobile/tablet */}
{sidebar && (
<button
onClick={() => {
const newVisible = !isSidebarVisible;
if (typeof window !== 'undefined') {
const width = window.innerWidth;
if (width < 768) {
setMobileSidebarVisible(newVisible);
} else if (width >= 768 && width < 1024) {
setTabletSidebarVisible(newVisible);
}
}
onSidebarToggle?.(newVisible);
}}
className={cn(
'lg:hidden',
'fixed top-4 left-4 z-50',
'p-2 rounded-lg',
'bg-bg-elevated border border-border-primary',
'text-text-primary hover:bg-bg-tertiary',
'transition-colors duration-[150ms]',
'shadow-lg',
'min-w-[44px] min-h-[44px]'
)}
aria-label={isSidebarVisible ? t('chat.closeSidebar', { defaultValue: 'Close sidebar' }) : t('chat.openSidebar', { defaultValue: 'Open sidebar' })}
>
{isSidebarVisible ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</button>
)}
{/* EN: Left Sidebar / VI: Sidebar trái */}
{sidebar && (
<aside
className={cn(
// EN: Base sidebar styles / VI: Style sidebar cơ bản
'flex flex-col bg-bg-secondary border-r border-border-primary transition-all duration-[250ms] ease-out',
// EN: Desktop: Fixed width 280px / VI: Desktop: Chiều rộng cố định 280px
'w-sidebar flex-shrink-0',
// EN: Mobile: Hide by default, show when sidebarVisible / VI: Mobile: Ẩn mặc định, hiện khi sidebarVisible
'max-md:fixed max-md:inset-y-0 max-md:left-0 max-md:z-40',
'max-md:transform max-md:transition-transform',
isSidebarVisible ? 'max-md:translate-x-0' : 'max-md:-translate-x-full',
// EN: Tablet: Show by default, toggleable / VI: Tablet: Hiện mặc định, có thể toggle
'md:block lg:block',
!isSidebarVisible && 'md:hidden'
)}
aria-label={t('chat.conversationSidebar', { defaultValue: 'Conversation sidebar' })}
>
{sidebar}
</aside>
)}
{/* EN: Main Chat Area / VI: Khu vực chat chính */}
<main
id="main-content"
className={cn(
// EN: Base main area styles / VI: Style khu vực chính cơ bản
'flex flex-col flex-1 overflow-hidden',
// EN: Center content with max-width constraint on desktop / VI: Căn giữa nội dung với giới hạn chiều rộng tối đa trên desktop
'md:items-center',
// EN: Mobile: Full width / VI: Mobile: Full width
'w-full'
)}
role="main"
aria-label={t('chat.mainChatArea', { defaultValue: 'Main chat area' })}
>
<div
className={cn(
// EN: Content container with max-width on desktop / VI: Container nội dung với chiều rộng tối đa trên desktop
'w-full flex flex-col h-full',
// EN: Mobile: Full width / VI: Mobile: Full width
'max-md:max-w-none',
// EN: Tablet: Medium width (60%) / VI: Tablet: Chiều rộng trung bình (60%)
'md:max-w-[60%] md:mx-auto',
// EN: Desktop: Max width 768px centered / VI: Desktop: Chiều rộng tối đa 768px căn giữa
'lg:max-w-chat-max lg:mx-auto'
)}
>
{children}
</div>
</main>
{/* EN: Right Panel / VI: Panel bên phải */}
{rightPanel && (
<aside
className={cn(
// EN: Base right panel styles / VI: Style panel bên phải cơ bản
'flex flex-col bg-bg-secondary border-l border-border-primary transition-all duration-[250ms] ease-out',
// EN: Desktop: Fixed width 320px, only show on large screens / VI: Desktop: Chiều rộng cố định 320px, chỉ hiện trên màn hình lớn
'w-80 flex-shrink-0',
// EN: Hide on small/medium screens / VI: Ẩn trên màn hình nhỏ/trung bình
'max-lg:hidden',
// EN: Show/hide based on rightPanelVisible prop / VI: Hiện/ẩn dựa trên prop rightPanelVisible
rightPanelVisible ? 'lg:flex' : 'lg:hidden'
)}
aria-label={t('chat.conversationSettingsPanel', { defaultValue: 'Conversation settings panel' })}
>
{rightPanel}
</aside>
)}
{/* EN: Mobile overlay when sidebar is visible / VI: Overlay mobile khi sidebar hiển thị */}
{sidebar && isSidebarVisible && (
<div
className={cn(
// EN: Overlay for mobile sidebar / VI: Overlay cho sidebar mobile
'fixed inset-0 bg-black/50 z-30',
// EN: Only show on mobile, not tablet / VI: Chỉ hiện trên mobile, không phải tablet
'md:hidden'
)}
onClick={() => {
if (typeof window !== 'undefined' && window.innerWidth < 768) {
setMobileSidebarVisible(false);
onSidebarToggle?.(false);
}
}}
aria-hidden="true"
/>
)}
</div>
);
}

View File

@@ -1,261 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { Button } from '@/features/shared/components/ui/button';
import { Input } from '@/features/shared/components/ui/input';
import { Avatar, AvatarFallback } from '@/features/shared/components/ui/avatar';
import { useAuthStore } from '@/stores/auth-store';
import { useTranslations } from 'next-intl';
import { useI18n } from '@/features/theme';
/**
* EN: Conversation interface
* VI: Interface cho Conversation
*/
export interface Conversation {
/** EN: Unique conversation ID / VI: ID duy nhất của conversation */
id: string;
/** EN: Conversation title / VI: Tiêu đề conversation */
title: string;
/** EN: Last message preview / VI: Xem trước tin nhắn cuối */
lastMessage?: string;
/** EN: Last message timestamp / VI: Timestamp tin nhắn cuối */
lastMessageAt?: Date;
/** EN: Whether conversation is selected / VI: Conversation có được chọn không */
isSelected?: boolean;
}
/**
* EN: ConversationSidebar component props
* VI: Props của component ConversationSidebar
*/
export interface ConversationSidebarProps {
/** EN: List of conversations / VI: Danh sách conversations */
conversations?: Conversation[];
/** EN: Selected conversation ID / VI: ID conversation được chọn */
selectedConversationId?: string;
/** EN: Callback when conversation is selected / VI: Callback khi conversation được chọn */
onSelectConversation?: (conversationId: string) => void;
/** EN: Callback when new chat button is clicked / VI: Callback khi nút new chat được click */
onNewChat?: () => void;
/** EN: Additional CSS classes / VI: Các class CSS bổ sung */
className?: string;
}
/**
* EN: ConversationSidebar component - Sidebar with conversation list, search, and user profile
* VI: Component ConversationSidebar - Sidebar với danh sách conversation, tìm kiếm, và profile người dùng
*
* Features:
* - New Chat button
* - Search conversations input
* - Scrollable conversation list
* - User profile section (sticky bottom)
*
* Tính năng:
* - Nút New Chat
* - Input tìm kiếm conversations
* - Danh sách conversation có thể scroll
* - Phần profile người dùng (sticky bottom)
*/
export function ConversationSidebar({
conversations = [],
selectedConversationId,
onSelectConversation,
onNewChat,
className,
}: ConversationSidebarProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const { locale } = useI18n();
// EN: Get current user from auth store / VI: Lấy user hiện tại từ auth store
const { user } = useAuthStore();
// EN: Search state / VI: State tìm kiếm
const [searchQuery, setSearchQuery] = React.useState('');
// EN: Filter conversations based on search query / VI: Lọc conversations dựa trên search query
const filteredConversations = React.useMemo(() => {
if (!searchQuery.trim()) {
return conversations;
}
const query = searchQuery.toLowerCase();
return conversations.filter(
(conv) =>
conv.title.toLowerCase().includes(query) ||
conv.lastMessage?.toLowerCase().includes(query)
);
}, [conversations, searchQuery]);
// EN: Format timestamp to relative time / VI: Format timestamp thành thời gian tương đối
const formatRelativeTime = (date?: Date): string => {
if (!date) return '';
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return t('chat.justNow');
if (diffMins < 60) return `${diffMins}m`;
if (diffHours < 24) return `${diffHours}h`;
if (diffDays < 7) return `${diffDays}d`;
return date.toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', { month: 'short', day: 'numeric' });
};
// EN: Get user initials for avatar fallback / VI: Lấy initials của user cho avatar fallback
const getUserInitials = (email?: string): string => {
if (!email) return 'U';
const parts = email.split('@')[0].split(/[._-]/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return email.substring(0, 2).toUpperCase();
};
return (
<div
className={cn(
// EN: Base styles - fixed width sidebar / VI: Styles cơ bản - sidebar cố định chiều rộng
'flex flex-col h-full w-sidebar bg-bg-secondary border-r border-border-primary',
className
)}
>
{/* EN: Header section with New Chat button / VI: Phần header với nút New Chat */}
<header className="p-4 border-b border-border-primary" role="banner">
<Button
variant="primary"
size="md"
className="w-full"
onClick={onNewChat}
aria-label={t('chat.newChat')}
>
{/* EN: Plus icon / VI: Icon dấu cộng */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
{t('chat.newChat')}
</Button>
</header>
{/* EN: Search section / VI: Phần tìm kiếm */}
<nav className="p-4 border-b border-border-primary" role="search" aria-label={t('chat.searchConversations')}>
<Input
type="search"
placeholder={t('chat.searchPlaceholder')}
value={searchQuery}
onChange={setSearchQuery}
className="w-full"
aria-label={t('chat.searchConversations')}
/>
</nav>
{/* EN: Conversations list (scrollable) / VI: Danh sách conversations (có thể scroll) */}
<nav className="flex-1 overflow-y-auto" role="navigation" aria-label={t('chat.conversationList')}>
{filteredConversations.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<p className="text-text-tertiary text-sm">
{searchQuery
? t('chat.noConversationsFound')
: t('chat.noConversations')}
</p>
</div>
) : (
<div className="p-2">
{filteredConversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => onSelectConversation?.(conversation.id)}
className={cn(
// EN: Base conversation item styles / VI: Styles cơ bản cho item conversation
'w-full text-left p-3 rounded-lg mb-2 transition-all duration-[150ms]',
// EN: Touch-friendly: Minimum 44px height, 8px spacing / VI: Thân thiện với chạm: Chiều cao tối thiểu 44px, khoảng cách 8px
'min-h-[44px]',
'hover:bg-bg-tertiary active:scale-[0.98] active:bg-bg-elevated',
'focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-secondary',
// EN: Selected state / VI: Trạng thái được chọn
conversation.id === selectedConversationId || conversation.isSelected
? 'bg-bg-tertiary border-l-3 border-accent-primary'
: 'bg-transparent'
)}
aria-label={`${t('chat.conversation')}: ${conversation.title}`}
>
{/* EN: Conversation title / VI: Tiêu đề conversation */}
<div className="flex items-start justify-between gap-2 mb-1">
<h3 className="text-sm font-medium text-text-primary truncate flex-1">
{conversation.title}
</h3>
{conversation.lastMessageAt && (
<span className="text-xs text-chat-timestamp whitespace-nowrap">
{formatRelativeTime(conversation.lastMessageAt)}
</span>
)}
</div>
{/* EN: Last message preview / VI: Xem trước tin nhắn cuối */}
{conversation.lastMessage && (
<p className="text-xs text-text-tertiary truncate">
{conversation.lastMessage}
</p>
)}
</button>
))}
</div>
)}
</nav>
{/* EN: User profile section (sticky bottom) / VI: Phần profile người dùng (sticky bottom) */}
{user && (
<div className="p-4 border-t border-border-primary bg-bg-secondary">
<div className="flex items-center gap-3">
<Avatar size="sm">
<AvatarFallback>{getUserInitials(user.email)}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary truncate">
{user.email}
</p>
<p className="text-xs text-text-tertiary truncate">
{user.role}
</p>
</div>
{/* EN: Settings icon / VI: Icon cài đặt */}
<button
className="p-2 rounded-md hover:bg-bg-tertiary transition-colors"
aria-label={t('settings.title')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-text-tertiary"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,11 +0,0 @@
/**
* EN: Chat feature exports
* VI: Export các thành phần chat
*/
export * from './chat-input';
export * from './chat-layout';
export * from './conversation-sidebar';
export * from './message-actions-menu';
export * from './message-bubble';
export * from './typing-indicator';

View File

@@ -1,6 +0,0 @@
/**
* EN: Chat feature library exports
* VI: Xuất thư viện của feature chat
*/
export * from './websocket';

View File

@@ -1,432 +0,0 @@
/**
* EN: WebSocket client for real-time messaging with automatic reconnection
* VI: WebSocket client cho real-time messaging với tự động kết nối lại
*/
/**
* EN: WebSocket connection state
* VI: Trạng thái kết nối WebSocket
*/
export enum WebSocketState {
/** EN: WebSocket is connecting / VI: WebSocket đang kết nối */
CONNECTING = 'CONNECTING',
/** EN: WebSocket is connected and ready / VI: WebSocket đã kết nối và sẵn sàng */
CONNECTED = 'CONNECTED',
/** EN: WebSocket connection is closed / VI: Kết nối WebSocket đã đóng */
CLOSED = 'CLOSED',
/** EN: WebSocket connection error / VI: Lỗi kết nối WebSocket */
ERROR = 'ERROR',
/** EN: WebSocket is reconnecting / VI: WebSocket đang kết nối lại */
RECONNECTING = 'RECONNECTING',
}
/**
* EN: WebSocket message event types
* VI: Các loại sự kiện message WebSocket
*/
export enum WebSocketMessageType {
/** EN: Text message / VI: Tin nhắn văn bản */
MESSAGE = 'message',
/** EN: Typing indicator / VI: Chỉ báo đang gõ */
TYPING = 'typing',
/** EN: Message read receipt / VI: Xác nhận đã đọc */
READ = 'read',
/** EN: User joined conversation / VI: Người dùng đã tham gia cuộc trò chuyện */
USER_JOINED = 'user_joined',
/** EN: User left conversation / VI: Người dùng đã rời cuộc trò chuyện */
USER_LEFT = 'user_left',
/** EN: Error message / VI: Tin nhắn lỗi */
ERROR = 'error',
/** EN: Connection acknowledgment / VI: Xác nhận kết nối */
ACK = 'ack',
}
/**
* EN: WebSocket message payload interface
* VI: Interface payload tin nhắn WebSocket
*/
export interface WebSocketMessage {
/** EN: Message type / VI: Loại tin nhắn */
type: WebSocketMessageType;
/** EN: Message payload / VI: Payload tin nhắn */
data?: Record<string, unknown>;
/** EN: Conversation ID / VI: ID cuộc trò chuyện */
conversationId?: string;
/** EN: Message ID / VI: ID tin nhắn */
messageId?: string;
/** EN: Timestamp / VI: Timestamp */
timestamp?: string;
}
/**
* EN: WebSocket event callbacks
* VI: Callbacks sự kiện WebSocket
*/
export interface WebSocketCallbacks {
/** EN: Called when WebSocket connection opens / VI: Được gọi khi kết nối WebSocket mở */
onOpen?: () => void;
/** EN: Called when WebSocket connection closes / VI: Được gọi khi kết nối WebSocket đóng */
onClose?: () => void;
/** EN: Called when WebSocket error occurs / VI: Được gọi khi xảy ra lỗi WebSocket */
onError?: (error: Event) => void;
/** EN: Called when message is received / VI: Được gọi khi nhận được tin nhắn */
onMessage?: (message: WebSocketMessage) => void;
/** EN: Called when connection state changes / VI: Được gọi khi trạng thái kết nối thay đổi */
onStateChange?: (state: WebSocketState) => void;
}
/**
* EN: WebSocket client configuration options
* VI: Tùy chọn cấu hình WebSocket client
*/
export interface WebSocketClientConfig {
/** EN: WebSocket server URL / VI: URL server WebSocket */
url: string;
/** EN: Enable automatic reconnection / VI: Bật tự động kết nối lại */
autoReconnect?: boolean;
/** EN: Maximum reconnection attempts (0 = unlimited) / VI: Số lần thử kết nối lại tối đa (0 = không giới hạn) */
maxReconnectAttempts?: number;
/** EN: Initial reconnection delay in milliseconds / VI: Độ trễ kết nối lại ban đầu tính bằng milliseconds */
reconnectDelay?: number;
/** EN: Maximum reconnection delay in milliseconds / VI: Độ trễ kết nối lại tối đa tính bằng milliseconds */
maxReconnectDelay?: number;
/** EN: Reconnection delay multiplier (exponential backoff) / VI: Hệ số nhân độ trễ kết nối lại (exponential backoff) */
reconnectDelayMultiplier?: number;
/** EN: Event callbacks / VI: Callbacks sự kiện */
callbacks?: WebSocketCallbacks;
}
/**
* EN: WebSocket client for real-time messaging
* VI: WebSocket client cho real-time messaging
*
* Features:
* - Automatic reconnection with exponential backoff
* - Authentication via JWT token
* - Event-based message handling
* - Connection state management
* - Heartbeat/ping-pong support (optional)
*/
export class WebSocketClient {
/** EN: WebSocket instance / VI: Instance WebSocket */
private ws: WebSocket | null = null;
/** EN: Current connection state / VI: Trạng thái kết nối hiện tại */
private state: WebSocketState = WebSocketState.CLOSED;
/** EN: Configuration options / VI: Tùy chọn cấu hình */
private config: Required<Omit<WebSocketClientConfig, 'callbacks'>> & { callbacks?: WebSocketCallbacks };
/** EN: Reconnection attempt counter / VI: Bộ đếm số lần thử kết nối lại */
private reconnectAttempts = 0;
/** EN: Current reconnection delay / VI: Độ trễ kết nối lại hiện tại */
private reconnectDelay = 0;
/** EN: Reconnection timeout ID / VI: ID timeout kết nối lại */
private reconnectTimeoutId: NodeJS.Timeout | null = null;
/** EN: Ping interval ID for heartbeat / VI: ID interval ping cho heartbeat */
private pingIntervalId: NodeJS.Timeout | null = null;
/**
* EN: Initialize WebSocket client with configuration
* VI: Khởi tạo WebSocket client với cấu hình
*
* @param config - WebSocket client configuration / Cấu hình WebSocket client
*/
constructor(config: WebSocketClientConfig) {
this.config = {
url: config.url,
autoReconnect: config.autoReconnect ?? true,
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
reconnectDelay: config.reconnectDelay ?? 1000,
maxReconnectDelay: config.maxReconnectDelay ?? 30000,
reconnectDelayMultiplier: config.reconnectDelayMultiplier ?? 1.5,
callbacks: config.callbacks,
};
this.reconnectDelay = this.config.reconnectDelay;
}
/**
* EN: Get current connection state
* VI: Lấy trạng thái kết nối hiện tại
*
* @returns Current WebSocket state / Trạng thái WebSocket hiện tại
*/
getState(): WebSocketState {
return this.state;
}
/**
* EN: Check if WebSocket is connected
* VI: Kiểm tra WebSocket đã kết nối chưa
*
* @returns True if connected / True nếu đã kết nối
*/
isConnected(): boolean {
return this.state === WebSocketState.CONNECTED && this.ws?.readyState === WebSocket.OPEN;
}
/**
* EN: Connect to WebSocket server
* VI: Kết nối tới server WebSocket
*
* @throws Error if connection fails / Ném lỗi nếu kết nối thất bại
*/
connect(): void {
// EN: Return if already connected or connecting
// VI: Trả về nếu đã kết nối hoặc đang kết nối
if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) {
return;
}
// EN: Get authentication token from localStorage
// VI: Lấy authentication token từ localStorage
const token = this.getAuthToken();
if (!token) {
throw new Error('No authentication token found. Please login first. / Không tìm thấy token xác thực. Vui lòng đăng nhập trước.');
}
// EN: Build WebSocket URL with authentication token
// VI: Tạo URL WebSocket với authentication token
const url = this.buildWebSocketUrl(this.config.url, token);
try {
this.setState(WebSocketState.CONNECTING);
this.ws = new WebSocket(url);
this.ws.onopen = this.handleOpen.bind(this);
this.ws.onclose = this.handleClose.bind(this);
this.ws.onerror = this.handleError.bind(this);
this.ws.onmessage = this.handleMessage.bind(this);
} catch (error) {
this.setState(WebSocketState.ERROR);
this.config.callbacks?.onError?.(error as Event);
throw error;
}
}
/**
* EN: Disconnect from WebSocket server
* VI: Ngắt kết nối khỏi server WebSocket
*/
disconnect(): void {
// EN: Disable auto-reconnect when manually disconnecting
// VI: Tắt tự động kết nối lại khi ngắt kết nối thủ công
this.config.autoReconnect = false;
// EN: Clear reconnection timeout
// VI: Xóa timeout kết nối lại
if (this.reconnectTimeoutId) {
clearTimeout(this.reconnectTimeoutId);
this.reconnectTimeoutId = null;
}
// EN: Clear ping interval
// VI: Xóa ping interval
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
}
// EN: Close WebSocket connection
// VI: Đóng kết nối WebSocket
if (this.ws) {
this.ws.close(1000, 'Client disconnect / Ngắt kết nối từ client');
this.ws = null;
}
this.setState(WebSocketState.CLOSED);
this.reconnectAttempts = 0;
this.reconnectDelay = this.config.reconnectDelay;
}
/**
* EN: Send message through WebSocket
* VI: Gửi tin nhắn qua WebSocket
*
* @param message - Message to send / Tin nhắn cần gửi
* @throws Error if not connected / Ném lỗi nếu chưa kết nối
*/
send(message: WebSocketMessage): void {
if (!this.isConnected() || !this.ws) {
throw new Error('WebSocket is not connected / WebSocket chưa kết nối');
}
try {
this.ws.send(JSON.stringify(message));
} catch (error) {
this.config.callbacks?.onError?.(error as Event);
throw error;
}
}
/**
* EN: Update event callbacks
* VI: Cập nhật event callbacks
*
* @param callbacks - New callbacks / Callbacks mới
*/
updateCallbacks(callbacks: WebSocketCallbacks): void {
this.config.callbacks = { ...this.config.callbacks, ...callbacks };
}
/**
* EN: Handle WebSocket open event
* VI: Xử lý sự kiện WebSocket mở
*/
private handleOpen(): void {
this.setState(WebSocketState.CONNECTED);
this.reconnectAttempts = 0;
this.reconnectDelay = this.config.reconnectDelay;
this.config.callbacks?.onOpen?.();
// EN: Start heartbeat/ping (optional, uncomment if server supports)
// VI: Bắt đầu heartbeat/ping (tùy chọn, bỏ comment nếu server hỗ trợ)
// this._startPing(); // EN: Unused for now / VI: Chưa sử dụng
}
/**
* EN: Handle WebSocket close event
* VI: Xử lý sự kiện WebSocket đóng
*/
private handleClose(event: CloseEvent): void {
this.setState(WebSocketState.CLOSED);
this.config.callbacks?.onClose?.();
// EN: Clear ping interval
// VI: Xóa ping interval
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
}
// EN: Attempt reconnection if not a normal closure and auto-reconnect is enabled
// VI: Thử kết nối lại nếu không phải đóng bình thường và auto-reconnect được bật
if (event.code !== 1000 && this.config.autoReconnect) {
this.attemptReconnect();
}
}
/**
* EN: Handle WebSocket error event
* VI: Xử lý sự kiện lỗi WebSocket
*/
private handleError(error: Event): void {
this.setState(WebSocketState.ERROR);
this.config.callbacks?.onError?.(error);
}
/**
* EN: Handle WebSocket message event
* VI: Xử lý sự kiện tin nhắn WebSocket
*/
private handleMessage(event: MessageEvent): void {
try {
const message: WebSocketMessage = JSON.parse(event.data);
this.config.callbacks?.onMessage?.(message);
} catch (error) {
console.error('Failed to parse WebSocket message / Không thể parse tin nhắn WebSocket:', error);
this.config.callbacks?.onError?.(error as Event);
}
}
/**
* EN: Attempt to reconnect to WebSocket server with exponential backoff
* VI: Thử kết nối lại tới server WebSocket với exponential backoff
*/
private attemptReconnect(): void {
// EN: Check if max reconnection attempts reached
// VI: Kiểm tra nếu đã đạt số lần thử kết nối lại tối đa
if (this.config.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
console.error(
`Max reconnection attempts reached (${this.config.maxReconnectAttempts}) / Đã đạt số lần thử kết nối lại tối đa (${this.config.maxReconnectAttempts})`
);
return;
}
this.reconnectAttempts++;
this.setState(WebSocketState.RECONNECTING);
// EN: Calculate exponential backoff delay
// VI: Tính toán độ trễ exponential backoff
const delay = Math.min(this.reconnectDelay, this.config.maxReconnectDelay);
console.log(
`Attempting to reconnect (${this.reconnectAttempts}/${this.config.maxReconnectAttempts || '∞'}) in ${delay}ms / Đang thử kết nối lại (${this.reconnectAttempts}/${this.config.maxReconnectAttempts || '∞'}) sau ${delay}ms`
);
// EN: Schedule reconnection attempt
// VI: Lên lịch thử kết nối lại
this.reconnectTimeoutId = setTimeout(() => {
this.reconnectDelay *= this.config.reconnectDelayMultiplier;
this.connect();
}, delay);
}
/**
* EN: Update connection state and trigger callback
* VI: Cập nhật trạng thái kết nối và trigger callback
*/
private setState(state: WebSocketState): void {
if (this.state !== state) {
this.state = state;
this.config.callbacks?.onStateChange?.(state);
}
}
/**
* EN: Get authentication token from localStorage
* VI: Lấy authentication token từ localStorage
*
* @returns Access token or null / Access token hoặc null
*/
private getAuthToken(): string | null {
if (typeof window !== 'undefined') {
return localStorage.getItem('accessToken');
}
return null;
}
/**
* EN: Build WebSocket URL with authentication token
* VI: Tạo URL WebSocket với authentication token
*
* @param baseUrl - Base WebSocket URL / URL WebSocket cơ sở
* @param token - Authentication token / Token xác thực
* @returns WebSocket URL with token / URL WebSocket với token
*/
private buildWebSocketUrl(baseUrl: string, token: string): string {
// EN: Convert HTTP/HTTPS URL to WS/WSS URL
// VI: Chuyển đổi URL HTTP/HTTPS sang WS/WSS URL
const wsUrl = baseUrl.replace(/^http/, 'ws');
const url = new URL(wsUrl);
// EN: Add token as query parameter (common pattern)
// VI: Thêm token như query parameter (pattern phổ biến)
url.searchParams.set('token', token);
return url.toString();
}
/**
* EN: Start ping interval for heartbeat (optional)
* VI: Bắt đầu ping interval cho heartbeat (tùy chọn)
* @internal - Currently unused, available for future use
*/
private _startPing(): void {
// EN: Send ping every 30 seconds to keep connection alive
// VI: Gửi ping mỗi 30 giây để giữ kết nối sống
this.pingIntervalId = setInterval(() => {
if (this.isConnected() && this.ws) {
this.ws.send(JSON.stringify({ type: WebSocketMessageType.ACK, data: { type: 'ping' } }));
}
}, 30000);
}
}
/**
* EN: Create WebSocket client instance
* VI: Tạo instance WebSocket client
*
* @param config - WebSocket client configuration / Cấu hình WebSocket client
* @returns WebSocket client instance / Instance WebSocket client
*/
export const createWebSocketClient = (config: WebSocketClientConfig): WebSocketClient => {
return new WebSocketClient(config);
};

View File

@@ -1,345 +0,0 @@
'use client';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/features/shared/components/ui/dropdown-menu';
import { cn } from '@/shared/lib/utils';
import { useTranslations } from 'next-intl';
/**
* EN: Message role type
* VI: Kiểu vai trò của message
*/
export type MessageRole = 'user' | 'ai' | 'system';
/**
* EN: Message feedback type
* VI: Kiểu feedback của message
*/
export type MessageFeedback = 'like' | 'dislike' | null;
/**
* EN: MessageActionsMenu component props
* VI: Props của component MessageActionsMenu
*/
export interface MessageActionsMenuProps {
/** EN: Message role (user/ai/system) / VI: Vai trò của message (user/ai/system) */
role: MessageRole;
/** EN: Message content to copy / VI: Nội dung message để copy */
messageContent: string;
/** EN: Current feedback state / VI: Trạng thái feedback hiện tại */
feedback?: MessageFeedback;
/** EN: Callback when copy action is triggered / VI: Callback khi action copy được kích hoạt */
onCopy?: () => void;
/** EN: Callback when edit action is triggered / VI: Callback khi action edit được kích hoạt */
onEdit?: () => void;
/** EN: Callback when delete action is triggered / VI: Callback khi action delete được kích hoạt */
onDelete?: () => void;
/** EN: Callback when regenerate action is triggered / VI: Callback khi action regenerate được kích hoạt */
onRegenerate?: () => void;
/** EN: Callback when like action is triggered / VI: Callback khi action like được kích hoạt */
onLike?: () => void;
/** EN: Callback when dislike action is triggered / VI: Callback khi action dislike được kích hoạt */
onDislike?: () => void;
/** EN: Callback when share action is triggered / VI: Callback khi action share được kích hoạt */
onShare?: () => void;
/** EN: Additional CSS classes / VI: Các class CSS bổ sung */
className?: string;
/** EN: Trigger element / VI: Element trigger */
children?: React.ReactNode;
}
/**
* EN: MessageActionsMenu component - Dropdown menu with message actions (copy, edit, delete, regenerate, like/dislike, share)
* VI: Component MessageActionsMenu - Dropdown menu với các action cho message (copy, edit, delete, regenerate, like/dislike, share)
*
* Features:
* - Copy message (all messages)
* - Edit (user messages only)
* - Delete (user messages only)
* - Regenerate (AI messages only)
* - Like/Dislike (feedback - all messages)
* - Share (all messages)
*
* Tính năng:
* - Copy message (tất cả messages)
* - Edit (chỉ user messages)
* - Delete (chỉ user messages)
* - Regenerate (chỉ AI messages)
* - Like/Dislike (feedback - tất cả messages)
* - Share (tất cả messages)
*/
export function MessageActionsMenu({
role,
messageContent,
feedback,
onCopy,
onEdit,
onDelete,
onRegenerate,
onLike,
onDislike,
onShare,
className,
children,
}: MessageActionsMenuProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
// EN: Copy to clipboard handler / VI: Handler copy vào clipboard
const handleCopy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(messageContent);
onCopy?.();
} catch (error) {
console.error('Failed to copy message:', error);
}
}, [messageContent, onCopy]);
// EN: Share handler using Web Share API / VI: Handler share sử dụng Web Share API
const handleShare = React.useCallback(async () => {
try {
if (navigator.share) {
await navigator.share({
text: messageContent,
title: t('chat.shareMessage'),
});
onShare?.();
} else {
// EN: Fallback to copy if share API is not available / VI: Fallback về copy nếu Share API không khả dụng
await handleCopy();
onShare?.();
}
} catch (error) {
// EN: User cancelled share or error occurred / VI: User hủy share hoặc có lỗi
if ((error as Error).name !== 'AbortError') {
console.error('Failed to share message:', error);
}
}
}, [messageContent, handleCopy, onShare]);
// EN: Determine if message is from user / VI: Xác định message có phải từ user không
const isUserMessage = role === 'user';
return (
<DropdownMenu>
<DropdownMenuTrigger asChild className={className}>
{children || (
<button
className={cn(
// EN: Base button styles - visible on hover / VI: Styles button cơ bản - hiển thị khi hover
'opacity-0 group-hover:opacity-100 transition-opacity duration-[150ms]',
'p-1.5 rounded-md hover:bg-bg-tertiary',
'text-text-tertiary hover:text-text-primary',
'focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2'
)}
aria-label={t('chat.messageActions', { defaultValue: 'Message actions' })}
>
{/* EN: More options icon (three dots) / VI: Icon thêm tùy chọn (ba chấm) */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
</button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{/* EN: Copy action - available for all messages / VI: Action Copy - có sẵn cho tất cả messages */}
<DropdownMenuItem onClick={handleCopy}>
{/* EN: Copy icon / VI: Icon copy */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2" />
</svg>
{t('chat.copy')}
</DropdownMenuItem>
{/* EN: Edit action - only for user messages / VI: Action Edit - chỉ cho user messages */}
{isUserMessage && onEdit && (
<DropdownMenuItem onClick={onEdit}>
{/* EN: Edit icon / VI: Icon edit */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
{t('chat.edit')}
</DropdownMenuItem>
)}
{/* EN: Regenerate action - only for AI messages / VI: Action Regenerate - chỉ cho AI messages */}
{!isUserMessage && onRegenerate && (
<DropdownMenuItem onClick={onRegenerate}>
{/* EN: Refresh/Regenerate icon / VI: Icon refresh/regenerate */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M8 16H3v5" />
</svg>
{t('chat.regenerate')}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{/* EN: Like/Dislike actions - available for all messages / VI: Actions Like/Dislike - có sẵn cho tất cả messages */}
{onLike && (
<DropdownMenuItem
onClick={onLike}
className={cn(feedback === 'like' && 'bg-bg-tertiary')}
>
{/* EN: Like icon (thumbs up) / VI: Icon like (thumbs up) */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill={feedback === 'like' ? 'currentColor' : 'none'}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M7 10v12" />
<path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z" />
</svg>
{t('chat.like')}
</DropdownMenuItem>
)}
{onDislike && (
<DropdownMenuItem
onClick={onDislike}
className={cn(feedback === 'dislike' && 'bg-bg-tertiary')}
>
{/* EN: Dislike icon (thumbs down) / VI: Icon dislike (thumbs down) */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill={feedback === 'dislike' ? 'currentColor' : 'none'}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M17 14V2" />
<path d="M9 18.12 10 14H4.17a2 2 0 0 0-1.92 2.56l2.33 8A2 2 0 0 0 6.5 22H20a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-2.76a2 2 0 0 0-1.79-1.11L12 2h0a3.13 3.13 0 0 0-3 3.88Z" />
</svg>
{t('chat.dislike')}
</DropdownMenuItem>
)}
{/* EN: Share action - available for all messages / VI: Action Share - có sẵn cho tất cả messages */}
{onShare && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleShare}>
{/* EN: Share icon / VI: Icon share */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" x2="12" y1="2" y2="15" />
</svg>
{t('chat.share')}
</DropdownMenuItem>
</>
)}
{/* EN: Delete action - only for user messages / VI: Action Delete - chỉ cho user messages */}
{isUserMessage && onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className="text-accent-error focus:text-accent-error focus:bg-bg-tertiary"
>
{/* EN: Delete icon / VI: Icon delete */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
{t('chat.delete')}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,564 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@/features/shared/components/ui/avatar';
import { useTranslations } from 'next-intl';
import { useI18n } from '@/features/theme';
/**
* EN: Message sender type
* VI: Loại người gửi tin nhắn
*/
export type MessageSender = 'user' | 'ai' | 'system';
/**
* EN: Message bubble props interface
* VI: Interface cho props của component Message bubble
*/
export interface MessageBubbleProps {
/**
* EN: Message sender type (user, ai, or system)
* VI: Loại người gửi tin nhắn (user, ai, hoặc system)
*/
sender: MessageSender;
/**
* EN: Message content / VI: Nội dung tin nhắn
*/
content: string;
/**
* EN: Message timestamp / VI: Thời gian tin nhắn
*/
timestamp?: Date | string;
/**
* EN: Author name / VI: Tên tác giả
*/
authorName?: string;
/**
* EN: Author avatar URL / VI: URL avatar tác giả
*/
authorAvatar?: string;
/**
* EN: Whether to show message actions on hover
* VI: Có hiển thị các hành động tin nhắn khi hover không
*/
showActions?: boolean;
/**
* EN: Callback when copy action is clicked
* VI: Callback khi hành động copy được click
*/
onCopy?: () => void;
/**
* EN: Callback when edit action is clicked (user messages only)
* VI: Callback khi hành động edit được click (chỉ tin nhắn user)
*/
onEdit?: () => void;
/**
* EN: Callback when delete action is clicked (user messages only)
* VI: Callback khi hành động delete được click (chỉ tin nhắn user)
*/
onDelete?: () => void;
/**
* EN: Callback when regenerate action is clicked (AI messages only)
* VI: Callback khi hành động regenerate được click (chỉ tin nhắn AI)
*/
onRegenerate?: () => void;
/**
* EN: Callback when like action is clicked
* VI: Callback khi hành động like được click
*/
onLike?: () => void;
/**
* EN: Callback when dislike action is clicked
* VI: Callback khi hành động dislike được click
*/
onDislike?: () => void;
/**
* EN: Callback when share action is clicked
* VI: Callback khi hành động share được click
*/
onShare?: () => void;
/**
* EN: Whether the message is liked
* VI: Tin nhắn có được like không
*/
isLiked?: boolean;
/**
* EN: Whether the message is disliked
* VI: Tin nhắn có được dislike không
*/
isDisliked?: boolean;
/**
* EN: Additional CSS classes
* VI: Các class CSS bổ sung
*/
className?: string;
}
/**
* EN: Format timestamp to readable string
* VI: Format timestamp thành chuỗi dễ đọc
*/
function formatTimestamp(timestamp: Date | string, t: (key: string, values?: any) => string, locale: string): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) {
return t('chat.justNow');
} else if (diffMins < 60) {
return t('chat.minutesAgo', { minutes: diffMins });
} else if (diffHours < 24) {
return t('chat.hoursAgo', { hours: diffHours });
} else if (diffDays < 7) {
return t('chat.daysAgo', { days: diffDays });
} else {
return date.toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
});
}
}
/**
* EN: Copy icon SVG
* VI: Icon SVG copy
*/
const CopyIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2" />
</svg>
);
/**
* EN: Edit icon SVG
* VI: Icon SVG edit
*/
const EditIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
);
/**
* EN: Delete icon SVG
* VI: Icon SVG delete
*/
const DeleteIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
);
/**
* EN: Regenerate icon SVG
* VI: Icon SVG regenerate
*/
const RegenerateIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
);
/**
* EN: Like icon SVG
* VI: Icon SVG like
*/
const LikeIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M7 10v12" />
<path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.67 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z" />
</svg>
);
/**
* EN: Dislike icon SVG
* VI: Icon SVG dislike
*/
const DislikeIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M17 14V2" />
<path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.67 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z" />
</svg>
);
/**
* EN: Share icon SVG
* VI: Icon SVG share
*/
const ShareIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" x2="12" y1="2" y2="15" />
</svg>
);
/**
* EN: Message bubble component - Displays chat messages with user/AI variants and actions
* VI: Component Message bubble - Hiển thị tin nhắn chat với các biến thể user/AI và các hành động
*
* Features:
* - User messages: Right aligned, blue bubble
* - AI messages: Left aligned, grey bubble
* - System messages: Centered, grey text
* - Message actions on hover: Copy, Edit, Delete, Regenerate, Like/Dislike, Share
* - Animation: Slide up + fade in
*
* Tính năng:
* - Tin nhắn user: Căn phải, bubble xanh
* - Tin nhắn AI: Căn trái, bubble xám
* - Tin nhắn hệ thống: Căn giữa, text xám
* - Hành động tin nhắn khi hover: Copy, Edit, Delete, Regenerate, Like/Dislike, Share
* - Animation: Trượt lên + fade in
*
* @example
* ```tsx
* <MessageBubble
* sender="user"
* content="Hello, AI!"
* timestamp={new Date()}
* authorName="John Doe"
* showActions
* onCopy={() => navigator.clipboard.writeText("Hello, AI!")}
* />
* ```
*/
export function MessageBubble({
sender,
content,
timestamp,
authorName,
authorAvatar,
showActions = true,
onCopy,
onEdit,
onDelete,
onRegenerate,
onLike,
onDislike,
onShare,
isLiked = false,
isDisliked = false,
className,
}: MessageBubbleProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const { locale } = useI18n();
// EN: System messages - centered, simple text / VI: Tin nhắn hệ thống - căn giữa, text đơn giản
if (sender === 'system') {
return (
<div
className={cn(
'flex justify-center items-center py-3 px-4',
className
)}
>
<span className="text-sm text-chat-timestamp">{content}</span>
</div>
);
}
const isUser = sender === 'user';
const [isHovered, setIsHovered] = React.useState(false);
return (
<div
className={cn(
// EN: Base message container / VI: Container tin nhắn cơ bản
'flex gap-3 px-4 py-3 group',
// EN: User messages: Right aligned / VI: Tin nhắn user: Căn phải
isUser && 'flex-row-reverse',
// EN: AI messages: Left aligned / VI: Tin nhắn AI: Căn trái
!isUser && 'flex-row',
// EN: Animation: Fade in / VI: Animation: Fade in
'opacity-0 animate-fadeIn',
className
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* EN: Avatar / VI: Avatar */}
{!isUser && (
<Avatar size="sm" className="flex-shrink-0">
{authorAvatar && (
<AvatarImage
src={authorAvatar}
alt={authorName ? t('chat.avatarOf', { name: authorName }) : t('chat.aiAssistantAvatar')}
/>
)}
<AvatarFallback aria-label={t('chat.aiAssistant')}>AI</AvatarFallback>
</Avatar>
)}
{/* EN: Message content container / VI: Container nội dung tin nhắn */}
<div
className={cn(
'flex flex-col',
// EN: Mobile: Full width (95% for padding) / VI: Mobile: Full width (95% cho padding)
'max-md:max-w-[95%]',
// EN: Tablet: Medium width (60%) / VI: Tablet: Chiều rộng trung bình (60%)
'md:max-w-[60%]',
// EN: Desktop: Max width 80% / VI: Desktop: Chiều rộng tối đa 80%
'lg:max-w-[80%]',
// EN: User messages: Align to right / VI: Tin nhắn user: Căn về phải
isUser && 'items-end',
// EN: AI messages: Align to left / VI: Tin nhắn AI: Căn về trái
!isUser && 'items-start'
)}
>
{/* EN: Message header (author, timestamp) / VI: Header tin nhắn (tác giả, thời gian) */}
{(authorName || timestamp) && (
<div
className={cn(
'flex items-center gap-2 mb-1 px-1',
isUser && 'flex-row-reverse'
)}
>
{authorName && (
<span className="text-xs font-medium text-text-secondary">
{authorName}
</span>
)}
{timestamp && (
<span className="text-xs text-chat-timestamp">
{formatTimestamp(timestamp, t, locale)}
</span>
)}
</div>
)}
{/* EN: Message bubble / VI: Bubble tin nhắn */}
<div
className={cn(
// EN: Base bubble styles / VI: Style bubble cơ bản
'relative rounded-lg px-4 py-2.5 break-words',
// EN: User bubble: Blue background / VI: Bubble user: Nền xanh
isUser &&
'bg-chat-user-bubble text-chat-user-text rounded-tr-none',
// EN: AI bubble: Grey background / VI: Bubble AI: Nền xám
!isUser &&
'bg-chat-ai-bubble text-chat-ai-text rounded-tl-none'
)}
>
{/* EN: Message text / VI: Text tin nhắn */}
<p className="text-sm leading-relaxed whitespace-pre-wrap">{content}</p>
{/* EN: Message actions (shown on hover) / VI: Hành động tin nhắn (hiện khi hover) */}
{showActions && isHovered && (
<div
className={cn(
// EN: Actions container / VI: Container hành động
'absolute flex items-center gap-1 rounded-md bg-bg-elevated border border-border-primary p-1 shadow-lg',
// EN: User messages: Position to top-right / VI: Tin nhắn user: Vị trí trên-phải
isUser && 'top-0 right-full mr-2 flex-row-reverse',
// EN: AI messages: Position to top-left / VI: Tin nhắn AI: Vị trí trên-trái
!isUser && 'top-0 left-full ml-2'
)}
>
{/* EN: Copy action / VI: Hành động copy */}
{onCopy && (
<button
onClick={onCopy}
className="p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={t('chat.copyMessage')}
title={t('chat.copy')}
>
<CopyIcon className="w-4 h-4 text-text-secondary" />
</button>
)}
{/* EN: Edit action (user messages only) / VI: Hành động edit (chỉ tin nhắn user) */}
{isUser && onEdit && (
<button
onClick={onEdit}
className="p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={t('chat.editMessage')}
title={t('chat.edit')}
>
<EditIcon className="w-4 h-4 text-text-secondary" />
</button>
)}
{/* EN: Delete action (user messages only) / VI: Hành động delete (chỉ tin nhắn user) */}
{isUser && onDelete && (
<button
onClick={onDelete}
className="p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={t('chat.deleteMessage')}
title={t('chat.delete')}
>
<DeleteIcon className="w-4 h-4 text-accent-error" />
</button>
)}
{/* EN: Regenerate action (AI messages only) / VI: Hành động regenerate (chỉ tin nhắn AI) */}
{!isUser && onRegenerate && (
<button
onClick={onRegenerate}
className="p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={t('chat.regenerateResponse')}
title={t('chat.regenerate')}
>
<RegenerateIcon className="w-4 h-4 text-text-secondary" />
</button>
)}
{/* EN: Like action / VI: Hành động like */}
{onLike && (
<button
onClick={onLike}
className={cn(
'p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center',
isLiked && 'text-accent-success'
)}
aria-label={t('chat.likeMessage')}
title={t('chat.like')}
>
<LikeIcon className="w-4 h-4" />
</button>
)}
{/* EN: Dislike action / VI: Hành động dislike */}
{onDislike && (
<button
onClick={onDislike}
className={cn(
'p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center',
isDisliked && 'text-accent-error'
)}
aria-label={t('chat.dislikeMessage')}
title={t('chat.dislike')}
>
<DislikeIcon className="w-4 h-4" />
</button>
)}
{/* EN: Share action / VI: Hành động share */}
{onShare && (
<button
onClick={onShare}
className="p-1.5 rounded hover:bg-bg-tertiary active:scale-[0.95] active:bg-bg-elevated transition-all duration-[150ms] min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={t('chat.shareMessage')}
title={t('chat.share')}
>
<ShareIcon className="w-4 h-4 text-text-secondary" />
</button>
)}
</div>
)}
</div>
</div>
{/* EN: User avatar (right side) / VI: Avatar user (bên phải) */}
{isUser && (
<Avatar size="sm" className="flex-shrink-0">
{authorAvatar && (
<AvatarImage
src={authorAvatar}
alt={authorName ? t('chat.avatarOf', { name: authorName }) : t('chat.userAvatar')}
/>
)}
<AvatarFallback aria-label={authorName ? t('chat.avatarOf', { name: authorName }) : t('chat.userAvatar')}>
{authorName
? authorName
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
: 'U'}
</AvatarFallback>
</Avatar>
)}
</div>
);
}

View File

@@ -1,120 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { useTranslations } from 'next-intl';
/**
* EN: TypingIndicator component props interface
* VI: Interface cho props của component TypingIndicator
*/
export interface TypingIndicatorProps {
/**
* EN: Number of dots to display (default: 3) / VI: Số lượng chấm hiển thị (mặc định: 3)
*/
dotCount?: number;
/**
* EN: Animation duration in milliseconds (default: 1400) / VI: Thời lượng animation tính bằng milliseconds (mặc định: 1400)
*/
duration?: number;
/**
* EN: Size of each dot in pixels (default: 8) / VI: Kích thước mỗi chấm tính bằng pixels (mặc định: 8)
*/
dotSize?: number;
/**
* EN: Color of the dots (default: uses theme tertiary text color) / VI: Màu của các chấm (mặc định: sử dụng màu text tertiary của theme)
*/
color?: string;
/**
* EN: Additional CSS classes / VI: Các class CSS bổ sung
*/
className?: string;
/**
* EN: Label for accessibility (default: "AI is typing...") / VI: Nhãn cho accessibility (mặc định: "AI đang nhập...")
*/
'aria-label'?: string;
}
/**
* EN: TypingIndicator component - Animated pulsing dots to indicate typing status
* VI: Component TypingIndicator - Các chấm nhấp nháy để hiển thị trạng thái đang nhập
*
* Features:
* - Pulsing dots animation with staggered delays
* - Configurable dot count, size, and color
* - Smooth animation using CSS keyframes
* - Accessibility support with aria-label
* - Follows design system theme colors
*
* Tính năng:
* - Animation các chấm nhấp nháy với độ trễ lệch nhau
* - Có thể cấu hình số lượng chấm, kích thước và màu sắc
* - Animation mượt mà sử dụng CSS keyframes
* - Hỗ trợ accessibility với aria-label
* - Tuân theo màu sắc của design system
*
* @example
* ```tsx
* <TypingIndicator />
* <TypingIndicator dotCount={4} dotSize={10} />
* <TypingIndicator color="#3B82F6" aria-label="User is typing..." />
* ```
*/
export function TypingIndicator({
dotCount = 3,
duration = 1400,
dotSize = 8,
color,
className,
'aria-label': ariaLabel,
}: TypingIndicatorProps) {
// EN: Translation hook / VI: Hook translation
const t = useTranslations();
const defaultAriaLabel = ariaLabel || t('chat.typing', { defaultValue: 'AI is typing...' });
// EN: Generate array of dot indices for rendering / VI: Tạo mảng các chỉ số chấm để render
const dots = React.useMemo(() => {
return Array.from({ length: dotCount }, (_, i) => i);
}, [dotCount]);
// EN: Calculate animation delay for each dot (staggered) / VI: Tính toán độ trễ animation cho mỗi chấm (lệch nhau)
const getDelay = (index: number) => {
// EN: Stagger delay by 200ms per dot / VI: Lệch độ trễ 200ms cho mỗi chấm
return (index * 200) / duration;
};
// EN: Generate inline styles for animation / VI: Tạo inline styles cho animation
const getDotStyle = (index: number): React.CSSProperties => {
return {
width: `${dotSize}px`,
height: `${dotSize}px`,
animationDelay: `${getDelay(index)}s`,
animationDuration: `${duration}ms`,
backgroundColor: color || 'var(--text-tertiary)', // EN: Default to theme tertiary text color / VI: Mặc định là màu text tertiary của theme
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
animationName: 'typing-pulse',
};
};
return (
<div
className={cn(
'flex items-center gap-2',
'px-4 py-3',
className
)}
role="status"
aria-label={defaultAriaLabel}
aria-live="polite"
>
{dots.map((index) => (
<span
key={index}
className="typing-dot rounded-full"
style={getDotStyle(index)}
aria-hidden="true"
/>
))}
</div>
);
}

View File

@@ -1,42 +0,0 @@
/**
* EN: Code block with syntax highlighting and copy-to-clipboard
* VI: Code block với syntax highlighting và copy-to-clipboard
*
* Displays formatted code with copy functionality
* Hiển thị code đã format với tính năng copy
*/
'use client';
import { useState } from 'react';
interface CodeBlockProps {
code: string;
language: string;
}
export default function CodeBlock({ code, language }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative">
{/* EN: Copy Button / VI: Nút copy */}
<button
onClick={handleCopy}
className="absolute top-2 right-2 glass-button text-xs px-3 py-1.5"
>
{copied ? '✓ Copied!' : 'Copy'}
</button>
{/* EN: Code Display / VI: Hiển thị code */}
<pre className="bg-bg-tertiary rounded-lg p-4 overflow-x-auto">
<code className="text-sm text-text-primary font-mono">{code}</code>
</pre>
</div>
);
}

View File

@@ -1,31 +0,0 @@
/**
* EN: Color swatch component to display CSS variable colors
* VI: Component color swatch để hiển thị màu từ CSS variable
*
* Shows color preview with variable name and value
* Hiển thị preview màu với tên biến và giá trị
*/
'use client';
interface ColorSwatchProps {
name: string;
var: string;
}
export default function ColorSwatch({ name, var: cssVar }: ColorSwatchProps) {
return (
<div className="glass-card p-4">
{/* EN: Color Preview Box / VI: Hộp preview màu */}
<div
className="w-full h-24 rounded-lg mb-3 border border-border-primary"
style={{ backgroundColor: cssVar }}
/>
{/* EN: Color Variable Name / VI: Tên biến màu */}
<p className="text-sm font-medium text-text-primary">{name}</p>
{/* EN: CSS Variable / VI: CSS Variable */}
<code className="text-xs text-text-tertiary font-mono">{cssVar}</code>
</div>
);
}

View File

@@ -1,64 +0,0 @@
/**
* EN: Component showcase wrapper with code preview
* VI: Wrapper showcase component kèm code preview
*
* Displays component demo with optional code snippet
* Hiển thị demo component với tùy chọn xem code snippet
*/
'use client';
import { useState } from 'react';
import CodeBlock from './CodeBlock';
interface ComponentShowcaseProps {
title: string;
description?: string;
code: string;
children: React.ReactNode;
darkBg?: boolean; // EN: For light components on dark background / VI: Cho light components trên dark background
}
export default function ComponentShowcase({
title,
description,
code,
children,
darkBg = false,
}: ComponentShowcaseProps) {
const [showCode, setShowCode] = useState(false);
return (
<div className="glass-card p-6">
{/* EN: Title and description / VI: Tiêu đề và mô tả */}
<div className="mb-4">
<h3 className="text-xl font-semibold text-text-primary">{title}</h3>
{description && (
<p className="text-text-tertiary text-sm mt-1">{description}</p>
)}
</div>
{/* EN: Component Preview Area / VI: Khu vực preview component */}
<div
className={`rounded-lg border border-border-primary p-8 mb-4 ${darkBg ? 'bg-bg-primary' : 'bg-bg-secondary'
}`}
>
{children}
</div>
{/* EN: Toggle Code Button / VI: Nút toggle code */}
<button
onClick={() => setShowCode(!showCode)}
className="glass-button text-sm"
>
{showCode ? 'Hide Code' : 'Show Code'}
</button>
{/* EN: Code Block / VI: Code block */}
{showCode && (
<div className="mt-4">
<CodeBlock code={code} language="tsx" />
</div>
)}
</div>
);
}

View File

@@ -1,39 +0,0 @@
/**
* EN: Section wrapper component for Mood Board sections
* VI: Component wrapper cho các sections của Mood Board
*
* Provides consistent layout and styling for each section
* Cung cấp layout và styling nhất quán cho mỗi section
*/
import React from 'react';
interface SectionWrapperProps {
id: string;
title: string;
description?: string;
children: React.ReactNode;
}
export default function SectionWrapper({
id,
title,
description,
children,
}: SectionWrapperProps) {
return (
<section id={id} className="scroll-mt-24">
{/* EN: Section Header / VI: Header của section */}
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight text-text-primary">
{title}
</h2>
{description && (
<p className="text-text-secondary mt-2">{description}</p>
)}
</div>
{/* EN: Section Content / VI: Nội dung section */}
<div className="space-y-6">{children}</div>
</section>
);
}

View File

@@ -1,55 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
/**
* EN: LiveRegion component props
* VI: Props của component LiveRegion
*/
export interface LiveRegionProps {
/**
* EN: Announcement message / VI: Thông báo
*/
message?: string;
/**
* EN: Priority level (polite or assertive) / VI: Mức độ ưu tiên (polite hoặc assertive)
*/
priority?: 'polite' | 'assertive';
/**
* EN: Additional CSS classes / VI: Các class CSS bổ sung
*/
className?: string;
}
/**
* EN: LiveRegion component - Announces changes to screen readers
* VI: Component LiveRegion - Thông báo thay đổi cho screen readers
*
* Features:
* - ARIA live region for announcements
* - Polite or assertive priority
* - Hidden visually but accessible to screen readers
*
* Tính năng:
* - Vùng live ARIA cho thông báo
* - Mức độ ưu tiên polite hoặc assertive
* - Ẩn về mặt thị giác nhưng có thể truy cập bởi screen readers
*
* @example
* ```tsx
* <LiveRegion message="New message received" priority="polite" />
* ```
*/
export function LiveRegion({ message, priority = 'polite', className }: LiveRegionProps) {
return (
<div
role="status"
aria-live={priority}
aria-atomic="true"
className={cn('sr-only', className)}
>
{message}
</div>
);
}

View File

@@ -1,41 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
/**
* EN: SkipToContent component - Skip link for keyboard navigation
* VI: Component SkipToContent - Link bỏ qua cho điều hướng bàn phím
*
* Features:
* - Appears on focus (keyboard navigation)
* - Links to main content area
* - WCAG 2.1 AA compliant
*
* Tính năng:
* - Xuất hiện khi focus (điều hướng bàn phím)
* - Link đến khu vực nội dung chính
* - Tuân thủ WCAG 2.1 AA
*/
export function SkipToContent() {
return (
<a
href="#main-content"
className={cn(
// EN: Hidden by default, visible on focus / VI: Ẩn mặc định, hiện khi focus
'sr-only focus:not-sr-only',
'fixed top-4 left-4 z-[100]',
'px-4 py-2',
'bg-accent-primary text-white',
'rounded-md',
'font-medium text-sm',
'shadow-lg',
'focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2',
'transition-all duration-[150ms]'
)}
aria-label="Skip to main content / Bỏ qua đến nội dung chính"
>
Skip to content / Bỏ qua đến nội dung
</a>
);
}

View File

@@ -1,140 +0,0 @@
'use client';
import React from 'react';
import Image from 'next/image';
import { useTheme } from '@/features/theme';
import { cn } from '@/shared/lib/utils';
import { BRAND, getBrandLogo } from '@/shared/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>
);
}

View File

@@ -1,6 +0,0 @@
/**
* EN: Brand components exports
* VI: Xuất các component thương hiệu
*/
export * from './brand-logo';

Some files were not shown because too many files have changed in this diff Show More