(getDeviceInfo);
-
- useEffect(() => {
- // Skip if running on server
- if (typeof window === 'undefined') {
- return;
- }
-
- let timeoutId: NodeJS.Timeout;
-
- /**
- * Handle window resize with debouncing
- * Debounce to avoid excessive re-renders during resize
- */
- const handleResize = () => {
- clearTimeout(timeoutId);
- timeoutId = setTimeout(() => {
- setDeviceInfo(getDeviceInfo());
- }, 150); // 150ms debounce
- };
-
- // Initial check
- setDeviceInfo(getDeviceInfo());
-
- // Listen for resize events
- window.addEventListener('resize', handleResize);
-
- // Cleanup
- return () => {
- clearTimeout(timeoutId);
- window.removeEventListener('resize', handleResize);
- };
- }, []);
-
- return deviceInfo;
-}
diff --git a/apps/web-client/src/features/shared/hooks/use-keyboard-shortcuts.ts b/apps/web-client/src/features/shared/hooks/use-keyboard-shortcuts.ts
deleted file mode 100644
index 784e9d58..00000000
--- a/apps/web-client/src/features/shared/hooks/use-keyboard-shortcuts.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-/**
- * EN: Keyboard shortcut configuration
- * VI: Cấu hình phím tắt bàn phím
- */
-export interface KeyboardShortcut {
- /**
- * EN: Key combination (e.g., 'ctrl+k', 'cmd+n') / VI: Tổ hợp phím (ví dụ: 'ctrl+k', 'cmd+n')
- */
- key: string;
- /**
- * EN: Callback function / VI: Hàm callback
- */
- handler: (e: KeyboardEvent) => void;
- /**
- * EN: Description for help menu / VI: Mô tả cho menu trợ giúp
- */
- description?: string;
- /**
- * EN: Whether to prevent default behavior / VI: Có ngăn hành vi mặc định không
- */
- preventDefault?: boolean;
-}
-
-/**
- * EN: useKeyboardShortcuts hook - Handles keyboard shortcuts
- * VI: Hook useKeyboardShortcuts - Xử lý phím tắt bàn phím
- *
- * Features:
- * - Custom keyboard shortcuts
- * - Ctrl/Cmd key support
- * - Prevent default behavior
- * - Help menu support
- *
- * Tính năng:
- * - Phím tắt tùy chỉnh
- * - Hỗ trợ phím Ctrl/Cmd
- * - Ngăn hành vi mặc định
- * - Hỗ trợ menu trợ giúp
- *
- * @example
- * ```tsx
- * useKeyboardShortcuts([
- * { key: 'ctrl+k', handler: () => openSearch(), description: 'Open search' },
- * { key: 'ctrl+n', handler: () => newChat(), description: 'New chat' },
- * ]);
- * ```
- */
-export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
- React.useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- shortcuts.forEach((shortcut) => {
- const keys = shortcut.key.toLowerCase().split('+');
- const ctrlKey = keys.includes('ctrl') || keys.includes('cmd');
- const shiftKey = keys.includes('shift');
- const altKey = keys.includes('alt');
- const key = keys[keys.length - 1];
-
- // EN: Check if modifier keys match / VI: Kiểm tra các phím modifier khớp
- const ctrlMatch = ctrlKey
- ? e.ctrlKey || e.metaKey
- : !e.ctrlKey && !e.metaKey;
- const shiftMatch = shiftKey ? e.shiftKey : !e.shiftKey;
- const altMatch = altKey ? e.altKey : !e.altKey;
- const keyMatch =
- key === e.key.toLowerCase() ||
- (key === 'enter' && e.key === 'Enter') ||
- (key === 'escape' && e.key === 'Escape') ||
- (key === 'space' && e.key === ' ');
-
- if (ctrlMatch && shiftMatch && altMatch && keyMatch) {
- if (shortcut.preventDefault) {
- e.preventDefault();
- }
- shortcut.handler(e);
- }
- });
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => {
- window.removeEventListener('keydown', handleKeyDown);
- };
- }, [shortcuts]);
-}
-
-/**
- * EN: Common keyboard shortcuts for chat interface
- * VI: Phím tắt phổ biến cho giao diện chat
- */
-export const CHAT_SHORTCUTS = {
- SEARCH: 'ctrl+k',
- NEW_CHAT: 'ctrl+n',
- SHOW_SHORTCUTS: 'ctrl+/',
- ESCAPE: 'escape',
-} as const;
diff --git a/apps/web-client/src/features/shared/i18n/en.json b/apps/web-client/src/features/shared/i18n/en.json
deleted file mode 100644
index 3169a12a..00000000
--- a/apps/web-client/src/features/shared/i18n/en.json
+++ /dev/null
@@ -1,419 +0,0 @@
-{
- "common": {
- "save": "Save",
- "cancel": "Cancel",
- "loading": "Loading...",
- "error": "Error",
- "success": "Success",
- "close": "Close",
- "confirm": "Confirm",
- "delete": "Delete",
- "edit": "Edit",
- "back": "Back",
- "next": "Next",
- "previous": "Previous",
- "submit": "Submit",
- "search": "Search",
- "filter": "Filter",
- "reset": "Reset",
- "apply": "Apply",
- "yes": "Yes",
- "no": "No",
- "ok": "OK",
- "user": "User",
- "optional": "Optional"
- },
- "auth": {
- "login": {
- "title": "Sign In",
- "description": "Enter your credentials to access your account",
- "email": "Email",
- "password": "Password",
- "rememberMe": "Remember me",
- "forgotPassword": "Forgot password?",
- "signUp": "Sign up",
- "signingIn": "Signing in...",
- "noAccount": "Don't have an account?",
- "loginFailed": "Login failed",
- "pageLabel": "Login page"
- },
- "register": {
- "title": "Sign Up",
- "createAccount": "Create Account",
- "description": "Create a new account to get started",
- "signUpToStart": "Sign up to get started",
- "email": "Email",
- "password": "Password",
- "confirmPassword": "Confirm Password",
- "fullName": "Full Name",
- "createStrongPassword": "Create a strong password",
- "reEnterPassword": "Re-enter your password",
- "agreeToTerms": "I agree to the",
- "termsAndConditions": "Terms and Conditions",
- "alreadyHaveAccount": "Already have an account?",
- "signIn": "Sign in",
- "signingUp": "Signing up...",
- "creatingAccount": "Creating account...",
- "registrationFailed": "Registration failed",
- "weak": "Weak",
- "fair": "Fair",
- "good": "Good",
- "strong": "Strong",
- "passwordStrength": "Password strength: {strength}"
- },
- "forgotPassword": {
- "title": "Forgot Password",
- "description": "Enter your email address and we'll send you a link to reset your password",
- "checkEmail": "Check your email for reset instructions",
- "email": "Email",
- "sendResetLink": "Send Reset Link",
- "sending": "Sending...",
- "backToLogin": "Back to Login",
- "resetLinkSent": "Reset link sent!",
- "resetLinkSentDetail": "We've sent a password reset link to {email}",
- "checkInbox": "Please check your inbox and follow the instructions to reset your password. If you don't see the email, check your spam folder.",
- "sendToAnotherEmail": "Send to another email",
- "failedToSend": "Failed to send reset link",
- "noAccount": "Don't have an account?",
- "signUp": "Sign up"
- }
- },
- "chat": {
- "newChat": "New Chat",
- "searchPlaceholder": "Search conversations...",
- "typeMessage": "Type your message...",
- "send": "Send message",
- "attachFile": "Attach file",
- "messageInput": "Message input",
- "sendHelp": "Press Enter to send, Shift+Enter for new line",
- "noConversations": "No conversations yet",
- "noConversationsFound": "No conversations found",
- "justNow": "Just now",
- "conversation": "Conversation",
- "conversationList": "Conversation list",
- "searchConversations": "Search conversations",
- "messages": "Chat messages",
- "startConversation": "Start a conversation",
- "startConversationDesc": "Type a message below to get started",
- "messageSent": "Message sent",
- "messageSendFailed": "Failed to send message",
- "newConversationCreated": "New conversation created",
- "switchedToConversation": "Switched to conversation",
- "messageCopied": "Message copied",
- "openSearch": "Open search",
- "loadingTypingIndicator": "Loading typing indicator",
- "typing": "AI is typing...",
- "justNow": "Just now",
- "minutesAgo": "{minutes}m ago",
- "hoursAgo": "{hours}h ago",
- "daysAgo": "{days}d ago",
- "copyMessage": "Copy message",
- "copy": "Copy",
- "editMessage": "Edit message",
- "edit": "Edit",
- "deleteMessage": "Delete message",
- "delete": "Delete",
- "regenerateResponse": "Regenerate response",
- "regenerate": "Regenerate",
- "likeMessage": "Like message",
- "like": "Like",
- "dislikeMessage": "Dislike message",
- "dislike": "Dislike",
- "shareMessage": "Share message",
- "share": "Share",
- "aiAssistant": "AI assistant",
- "aiAssistantAvatar": "AI assistant avatar",
- "userAvatar": "User avatar",
- "avatarOf": "Avatar of {name}",
- "messageActions": "Message actions",
- "closeSidebar": "Close sidebar",
- "openSidebar": "Open sidebar",
- "conversationSidebar": "Conversation sidebar",
- "mainChatArea": "Main chat area",
- "conversationSettingsPanel": "Conversation settings panel"
- },
- "settings": {
- "title": "Settings",
- "description": "Manage your account settings and preferences",
- "navigation": "Settings navigation",
- "profile": {
- "title": "Profile",
- "label": "Profile",
- "firstName": "First Name",
- "lastName": "Last Name",
- "phone": "Phone",
- "bio": "Bio",
- "email": "Email",
- "username": "Username",
- "verified": "Verified",
- "notVerified": "Not verified",
- "changeAvatar": "Change Avatar",
- "uploadAvatar": "Upload Avatar",
- "saveChanges": "Save Changes",
- "changesSaved": "Changes saved successfully",
- "failedToFetch": "Failed to fetch profile",
- "failedToUpdate": "Failed to update profile",
- "failedToUpload": "Failed to upload avatar",
- "uploadSuccess": "Avatar uploaded successfully",
- "selectImageFile": "Please select an image file",
- "imageSizeLimit": "Image size must be less than 5MB",
- "updateInfo": "Update your profile information and avatar",
- "enterFirstName": "Enter your first name",
- "enterLastName": "Enter your last name",
- "enterPhone": "Enter your phone number",
- "bioPlaceholder": "Tell us about yourself",
- "emailCannotChange": "Email cannot be changed",
- "usernameCannotChange": "Username cannot be changed",
- "notSet": "Not set"
- },
- "preferences": {
- "title": "Preferences",
- "label": "Preferences",
- "description": "Customize your experience and application settings",
- "languageAndTheme": "Language & Theme",
- "languageAndThemeDesc": "Choose your preferred language and appearance",
- "language": "Language",
- "languageHelper": "Select your preferred language",
- "theme": "Theme",
- "themeHelper": "Choose your preferred theme",
- "light": "Light",
- "dark": "Dark",
- "system": "System",
- "chatSettings": "Chat Settings",
- "chatSettingsDesc": "Customize your chat experience",
- "autoScroll": "Auto-scroll",
- "autoScrollDesc": "Automatically scroll to the latest message",
- "showTimestamps": "Show timestamps",
- "showTimestampsDesc": "Display message timestamps",
- "messageGrouping": "Message grouping",
- "messageGroupingHelper": "Choose how messages are grouped",
- "none": "None",
- "byAuthor": "By author",
- "byTime": "By time",
- "fontSize": "Font size",
- "fontSizeHelper": "Choose your preferred font size",
- "small": "Small",
- "medium": "Medium",
- "large": "Large",
- "extraLarge": "Extra Large",
- "accessibility": "Accessibility",
- "accessibilityDesc": "Improve accessibility and usability",
- "highContrast": "High contrast mode",
- "highContrastDesc": "Increase color contrast for better visibility",
- "screenReader": "Screen reader optimizations",
- "screenReaderDesc": "Enable additional ARIA labels and announcements",
- "savePreferences": "Save Preferences",
- "preferencesSaved": "Preferences saved successfully"
- },
- "security": {
- "title": "Security",
- "label": "Security",
- "changePassword": "Change Password",
- "currentPassword": "Current Password",
- "currentPasswordRequired": "Current password is required",
- "newPassword": "New Password",
- "confirmPassword": "Confirm Password",
- "passwordChanged": "Password changed successfully",
- "passwordChangeFailed": "Failed to change password",
- "twoFactorAuth": "Two-Factor Authentication",
- "enable2FA": "Enable 2FA",
- "disable2FA": "Disable 2FA",
- "confirmDisable2FA": "Are you sure you want to disable 2FA?",
- "2FAEnabled": "2FA enabled successfully",
- "2FADisabled": "2FA disabled successfully",
- "enable2FAFailed": "Failed to enable 2FA",
- "disable2FAFailed": "Failed to disable 2FA",
- "invalidVerificationCode": "Invalid verification code",
- "codeMustBe6Digits": "Code must be 6 digits",
- "activeSessions": "Active Sessions",
- "deviceName": "Device Name",
- "ipAddress": "IP Address",
- "lastActivity": "Last activity",
- "created": "Created",
- "revoke": "Revoke",
- "revokeSession": "Revoke Session",
- "never": "Never",
- "noActiveSessions": "No active sessions",
- "2FAStatus": "2FA Status",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "twoFactorDesc": "Add an extra layer of security to your account",
- "twoFactorInstructions": "Two-factor authentication adds an extra layer of security. When enabled, you'll need to enter a code from your authenticator app in addition to your password when signing in.",
- "manageSettings": "Manage your account security settings",
- "updatePasswordDesc": "Update your password to keep your account secure",
- "enterCurrentPassword": "Enter current password",
- "enterNewPassword": "Enter new password",
- "confirmNewPassword": "Confirm new password",
- "updatePassword": "Update Password",
- "manageDevices": "Manage devices that are signed in to your account",
- "unknownDevice": "Unknown Device",
- "confirmRevokeAll": "Are you sure you want to revoke all other sessions?",
- "revokeAllOtherSessions": "Revoke All Other Sessions",
- "setup2FA": "Set Up Two-Factor Authentication",
- "scanQRCode": "Scan this QR code with your authenticator app",
- "qrCodeAlt": "QR Code for two-factor authentication",
- "cantScan": "Can't scan? Enter this code manually",
- "verificationCode": "Verification Code",
- "enter6DigitCode": "Enter 6-digit code",
- "enterCodeFromApp": "Enter the 6-digit code from your authenticator app",
- "verifyAndEnable": "Verify & Enable"
- },
- "notifications": {
- "title": "Notifications",
- "label": "Notifications"
- },
- "billing": {
- "title": "Billing",
- "label": "Billing"
- },
- "apiKeys": {
- "title": "API Keys",
- "label": "API Keys",
- "manageKeys": "Manage your API keys for programmatic access",
- "yourApiKeys": "Your API Keys",
- "createAndManage": "Create and manage API keys for accessing the API",
- "createApiKey": "Create API Key",
- "noApiKeys": "No API keys yet",
- "createFirstKey": "Create your first API key to get started",
- "name": "Name",
- "nameRequired": "Name is required",
- "description": "Description",
- "created": "Created",
- "lastUsed": "Last used",
- "expires": "Expires",
- "never": "Never",
- "copy": "Copy",
- "copied": "Copied",
- "delete": "Delete",
- "show": "Show",
- "hide": "Hide",
- "failedToLoad": "Failed to load API keys",
- "failedToCreate": "Failed to create API key",
- "failedToDelete": "Failed to delete API key",
- "deletedSuccessfully": "API key deleted successfully",
- "confirmDelete": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
- "newApiKeyCreated": "New API Key Created",
- "saveKeySecurely": "Make sure to copy your API key now. You won't be able to see it again!",
- "keyCopied": "API key copied to clipboard",
- "namePlaceholder": "e.g., Production Key, Development Key",
- "nameHelper": "A descriptive name for this API key",
- "descriptionPlaceholder": "Optional description for this API key",
- "newKeyFor": "Your new API key for \"{name}\"",
- "important": "Important",
- "securityBestPractices": "Security Best Practices",
- "practice1": "Keep your API keys secure and never share them publicly",
- "practice2": "Use environment variables or secure secret management tools",
- "practice3": "Rotate your API keys regularly",
- "practice4": "Delete unused API keys immediately",
- "practice5": "If a key is compromised, revoke it immediately and create a new one",
- "createForAccess": "Create a new API key for programmatic access",
- "copyApiKey": "Copy API Key",
- "iveCopied": "I've copied the key"
- }
- },
- "validation": {
- "required": "This field is required",
- "email": "Invalid email format",
- "emailRequired": "Email is required",
- "password": "Password is required",
- "passwordMin": "Password must be at least 8 characters",
- "passwordConfirm": "Passwords do not match",
- "passwordConfirmRequired": "Please confirm your password",
- "minLength": "Must be at least {min} characters",
- "maxLength": "Must be at most {max} characters",
- "invalidFormat": "Invalid format",
- "fullNameRequired": "Full name is required",
- "fullNameMin": "Full name must be at least 2 characters",
- "fullNameMax": "Full name must be less than 100 characters",
- "passwordUppercase": "Password must contain at least one uppercase letter",
- "passwordLowercase": "Password must contain at least one lowercase letter",
- "passwordNumber": "Password must contain at least one number",
- "passwordSpecial": "Password must contain at least one special character",
- "termsRequired": "You must accept the terms and conditions"
- },
- "errors": {
- "generic": "An error occurred",
- "networkError": "Network error. Please check your connection.",
- "unauthorized": "You are not authorized to perform this action",
- "notFound": "Resource not found",
- "serverError": "Server error. Please try again later.",
- "unknown": "An unknown error occurred"
- },
- "home": {
- "title": "GoodGo Platform",
- "description": "Experience the next generation of minimal intelligence.",
- "welcome": "Welcome, {email}!",
- "role": "Role: {role}",
- "pleaseLogin": "Please log in to continue."
- },
- "account": {
- "title": "My Account",
- "description": "Manage your account settings, view statistics, and monitor activity",
- "editProfile": "Edit Profile",
- "verified": "Verified",
- "stats": {
- "totalChats": "Total Chats",
- "activeSessions": "Active Sessions",
- "apiCalls": "API Calls (This Month)",
- "uptime": "Uptime",
- "vsLastMonth": "vs last month"
- },
- "quickActions": "Quick Actions",
- "quickActionsDescription": "Common account management tasks",
- "actions": {
- "updateProfile": "Update Profile",
- "manageApiKeys": "Manage API Keys",
- "billing": "Billing & Subscription",
- "notifications": "Notification Settings"
- },
- "info": {
- "title": "Account Information",
- "description": "Your account details and verification status",
- "email": "Email",
- "username": "Username",
- "memberSince": "Member Since",
- "plan": "Plan"
- },
- "recentActivity": "Recent Activity",
- "recentActivityDescription": "Your recent account activities and changes",
- "viewAllActivity": "View All Activity",
- "activity": {
- "apiKeyCreated": "Created new API key",
- "profileUpdated": "Updated profile information",
- "chatCreated": "Started new conversation",
- "settingsChanged": "Changed notification preferences"
- },
- "security": {
- "title": "Security Status",
- "description": "Your account security settings and recommendations",
- "score": "Security Score",
- "excellent": "Excellent! Your account is well secured.",
- "improve": "Complete remaining items to improve security.",
- "emailVerified": "Email Verified",
- "twoFactorEnabled": "Two-Factor Authentication",
- "phoneVerified": "Phone Verified",
- "recoveryCodes": "Recovery Codes Generated",
- "enabled": "Enabled",
- "enable": "Enable"
- }
- },
- "footer": {
- "tagline": "Build, deploy, and scale microservices with confidence. Enterprise-grade platform for modern development teams.",
- "product": "Product",
- "company": "Company",
- "support": "Support",
- "features": "Features",
- "pricing": "Pricing",
- "documentation": "Documentation",
- "about": "About",
- "blog": "Blog",
- "careers": "Careers",
- "helpCenter": "Help Center",
- "contact": "Contact",
- "status": "Status",
- "privacyPolicy": "Privacy Policy",
- "termsOfService": "Terms of Service",
- "copyright": "© {year} GoodGo Platform. All rights reserved.",
- "followUs": "Follow us"
- }
-}
\ No newline at end of file
diff --git a/apps/web-client/src/features/shared/i18n/vi.json b/apps/web-client/src/features/shared/i18n/vi.json
deleted file mode 100644
index ed251445..00000000
--- a/apps/web-client/src/features/shared/i18n/vi.json
+++ /dev/null
@@ -1,419 +0,0 @@
-{
- "common": {
- "save": "Lưu",
- "cancel": "Hủy",
- "loading": "Đang tải...",
- "error": "Lỗi",
- "success": "Thành công",
- "close": "Đóng",
- "confirm": "Xác nhận",
- "delete": "Xóa",
- "edit": "Chỉnh sửa",
- "back": "Quay lại",
- "next": "Tiếp theo",
- "previous": "Trước đó",
- "submit": "Gửi",
- "search": "Tìm kiếm",
- "filter": "Lọc",
- "reset": "Đặt lại",
- "apply": "Áp dụng",
- "yes": "Có",
- "no": "Không",
- "ok": "OK",
- "user": "Người dùng",
- "optional": "Tùy chọn"
- },
- "auth": {
- "login": {
- "title": "Đăng nhập",
- "description": "Nhập thông tin đăng nhập để truy cập tài khoản",
- "email": "Email",
- "password": "Mật khẩu",
- "rememberMe": "Nhớ đăng nhập",
- "forgotPassword": "Quên mật khẩu?",
- "signUp": "Đăng ký",
- "signingIn": "Đang đăng nhập...",
- "noAccount": "Chưa có tài khoản?",
- "loginFailed": "Đăng nhập thất bại",
- "pageLabel": "Trang đăng nhập"
- },
- "register": {
- "title": "Đăng ký",
- "createAccount": "Tạo tài khoản",
- "description": "Tạo tài khoản mới để bắt đầu",
- "signUpToStart": "Đăng ký để bắt đầu",
- "email": "Email",
- "password": "Mật khẩu",
- "confirmPassword": "Xác nhận mật khẩu",
- "fullName": "Họ tên",
- "createStrongPassword": "Tạo mật khẩu mạnh",
- "reEnterPassword": "Nhập lại mật khẩu",
- "agreeToTerms": "Tôi đồng ý với",
- "termsAndConditions": "Điều khoản và điều kiện",
- "alreadyHaveAccount": "Đã có tài khoản?",
- "signIn": "Đăng nhập",
- "signingUp": "Đang đăng ký...",
- "creatingAccount": "Đang tạo tài khoản...",
- "registrationFailed": "Đăng ký thất bại",
- "weak": "Yếu",
- "fair": "Trung bình",
- "good": "Tốt",
- "strong": "Mạnh",
- "passwordStrength": "Độ mạnh mật khẩu: {strength}"
- },
- "forgotPassword": {
- "title": "Quên mật khẩu",
- "description": "Nhập địa chỉ email của bạn và chúng tôi sẽ gửi cho bạn liên kết để đặt lại mật khẩu",
- "checkEmail": "Kiểm tra email để xem hướng dẫn đặt lại",
- "email": "Email",
- "sendResetLink": "Gửi link đặt lại",
- "sending": "Đang gửi...",
- "backToLogin": "Quay lại đăng nhập",
- "resetLinkSent": "Link đặt lại đã được gửi!",
- "resetLinkSentDetail": "Chúng tôi đã gửi link đặt lại mật khẩu đến {email}",
- "checkInbox": "Vui lòng kiểm tra hộp thư và làm theo hướng dẫn để đặt lại mật khẩu. Nếu bạn không thấy email, hãy kiểm tra thư mục spam.",
- "sendToAnotherEmail": "Gửi đến email khác",
- "failedToSend": "Gửi link đặt lại thất bại",
- "noAccount": "Chưa có tài khoản?",
- "signUp": "Đăng ký"
- }
- },
- "chat": {
- "newChat": "Cuộc trò chuyện mới",
- "searchPlaceholder": "Tìm kiếm...",
- "typeMessage": "Nhập tin nhắn...",
- "send": "Gửi tin nhắn",
- "attachFile": "Đính kèm file",
- "messageInput": "Ô nhập tin nhắn",
- "sendHelp": "Nhấn Enter để gửi, Shift+Enter để xuống dòng",
- "noConversations": "Chưa có cuộc trò chuyện nào",
- "noConversationsFound": "Không tìm thấy cuộc trò chuyện",
- "justNow": "Vừa xong",
- "conversation": "Cuộc trò chuyện",
- "conversationList": "Danh sách cuộc trò chuyện",
- "searchConversations": "Tìm kiếm cuộc trò chuyện",
- "messages": "Tin nhắn chat",
- "startConversation": "Bắt đầu cuộc trò chuyện",
- "startConversationDesc": "Nhập tin nhắn bên dưới để bắt đầu",
- "messageSent": "Tin nhắn đã gửi",
- "messageSendFailed": "Không thể gửi tin nhắn",
- "newConversationCreated": "Đã tạo cuộc trò chuyện mới",
- "switchedToConversation": "Đã chuyển sang cuộc trò chuyện",
- "messageCopied": "Đã sao chép tin nhắn",
- "openSearch": "Mở tìm kiếm",
- "loadingTypingIndicator": "Đang tải chỉ báo đang gõ",
- "typing": "AI đang nhập...",
- "justNow": "Vừa xong",
- "minutesAgo": "{minutes} phút trước",
- "hoursAgo": "{hours} giờ trước",
- "daysAgo": "{days} ngày trước",
- "copyMessage": "Sao chép tin nhắn",
- "copy": "Sao chép",
- "editMessage": "Chỉnh sửa tin nhắn",
- "edit": "Chỉnh sửa",
- "deleteMessage": "Xóa tin nhắn",
- "delete": "Xóa",
- "regenerateResponse": "Tạo lại phản hồi",
- "regenerate": "Tạo lại",
- "likeMessage": "Thích tin nhắn",
- "like": "Thích",
- "dislikeMessage": "Không thích tin nhắn",
- "dislike": "Không thích",
- "shareMessage": "Chia sẻ tin nhắn",
- "share": "Chia sẻ",
- "aiAssistant": "Trợ lý AI",
- "aiAssistantAvatar": "Avatar trợ lý AI",
- "userAvatar": "Avatar người dùng",
- "avatarOf": "Avatar của {name}",
- "messageActions": "Các hành động cho message",
- "closeSidebar": "Đóng sidebar",
- "openSidebar": "Mở sidebar",
- "conversationSidebar": "Sidebar cuộc trò chuyện",
- "mainChatArea": "Khu vực chat chính",
- "conversationSettingsPanel": "Panel cài đặt cuộc trò chuyện"
- },
- "settings": {
- "title": "Cài đặt",
- "description": "Quản lý cài đặt và tùy chọn tài khoản",
- "navigation": "Điều hướng Settings",
- "profile": {
- "title": "Hồ sơ",
- "label": "Hồ sơ",
- "firstName": "Tên",
- "lastName": "Họ",
- "phone": "Số điện thoại",
- "bio": "Tiểu sử",
- "email": "Email",
- "username": "Tên người dùng",
- "verified": "Đã xác thực",
- "notVerified": "Chưa xác thực",
- "changeAvatar": "Đổi Avatar",
- "uploadAvatar": "Tải lên Avatar",
- "saveChanges": "Lưu thay đổi",
- "changesSaved": "Đã lưu thay đổi thành công",
- "failedToFetch": "Không thể lấy profile",
- "failedToUpdate": "Không thể cập nhật profile",
- "failedToUpload": "Không thể upload avatar",
- "uploadSuccess": "Avatar đã được upload thành công",
- "selectImageFile": "Vui lòng chọn file ảnh",
- "imageSizeLimit": "Kích thước ảnh phải nhỏ hơn 5MB",
- "updateInfo": "Cập nhật thông tin hồ sơ và avatar",
- "enterFirstName": "Nhập tên của bạn",
- "enterLastName": "Nhập họ của bạn",
- "enterPhone": "Nhập số điện thoại của bạn",
- "bioPlaceholder": "Hãy cho chúng tôi biết về bạn",
- "emailCannotChange": "Email không thể thay đổi",
- "usernameCannotChange": "Tên người dùng không thể thay đổi",
- "notSet": "Chưa đặt"
- },
- "preferences": {
- "title": "Tùy chọn",
- "label": "Tùy chọn",
- "description": "Tùy chỉnh trải nghiệm và cài đặt ứng dụng",
- "languageAndTheme": "Ngôn ngữ & Giao diện",
- "languageAndThemeDesc": "Chọn ngôn ngữ và giao diện ưa thích",
- "language": "Ngôn ngữ",
- "languageHelper": "Chọn ngôn ngữ ưa thích",
- "theme": "Giao diện",
- "themeHelper": "Chọn giao diện ưa thích",
- "light": "Sáng",
- "dark": "Tối",
- "system": "Hệ thống",
- "chatSettings": "Cài đặt Chat",
- "chatSettingsDesc": "Tùy chỉnh trải nghiệm chat",
- "autoScroll": "Tự động cuộn",
- "autoScrollDesc": "Tự động cuộn đến tin nhắn mới nhất",
- "showTimestamps": "Hiển thị thời gian",
- "showTimestampsDesc": "Hiển thị thời gian của tin nhắn",
- "messageGrouping": "Nhóm tin nhắn",
- "messageGroupingHelper": "Chọn cách nhóm tin nhắn",
- "none": "Không",
- "byAuthor": "Theo tác giả",
- "byTime": "Theo thời gian",
- "fontSize": "Kích thước chữ",
- "fontSizeHelper": "Chọn kích thước chữ ưa thích",
- "small": "Nhỏ",
- "medium": "Trung bình",
- "large": "Lớn",
- "extraLarge": "Rất lớn",
- "accessibility": "Khả năng truy cập",
- "accessibilityDesc": "Cải thiện khả năng truy cập và sử dụng",
- "highContrast": "Chế độ tương phản cao",
- "highContrastDesc": "Tăng độ tương phản màu sắc để dễ nhìn hơn",
- "screenReader": "Tối ưu cho screen reader",
- "screenReaderDesc": "Bật thêm ARIA labels và thông báo",
- "savePreferences": "Lưu tùy chọn",
- "preferencesSaved": "Đã lưu preferences thành công"
- },
- "security": {
- "title": "Bảo mật",
- "label": "Bảo mật",
- "changePassword": "Đổi mật khẩu",
- "currentPassword": "Mật khẩu hiện tại",
- "currentPasswordRequired": "Mật khẩu hiện tại là bắt buộc",
- "newPassword": "Mật khẩu mới",
- "confirmPassword": "Xác nhận mật khẩu",
- "passwordChanged": "Mật khẩu đã được thay đổi thành công",
- "passwordChangeFailed": "Không thể thay đổi mật khẩu",
- "twoFactorAuth": "Xác thực hai yếu tố",
- "enable2FA": "Bật 2FA",
- "disable2FA": "Tắt 2FA",
- "confirmDisable2FA": "Bạn có chắc chắn muốn tắt 2FA?",
- "2FAEnabled": "2FA đã được bật thành công",
- "2FADisabled": "2FA đã được tắt thành công",
- "enable2FAFailed": "Không thể bật 2FA",
- "disable2FAFailed": "Không thể tắt 2FA",
- "invalidVerificationCode": "Mã xác thực không hợp lệ",
- "codeMustBe6Digits": "Mã phải có 6 chữ số",
- "activeSessions": "Phiên đăng nhập đang hoạt động",
- "deviceName": "Tên thiết bị",
- "ipAddress": "Địa chỉ IP",
- "lastActivity": "Hoạt động cuối",
- "created": "Đã tạo",
- "revoke": "Thu hồi",
- "revokeSession": "Thu hồi phiên",
- "never": "Không bao giờ",
- "noActiveSessions": "Không có phiên đăng nhập đang hoạt động",
- "2FAStatus": "Trạng thái 2FA",
- "enabled": "Đã bật",
- "disabled": "Đã tắt",
- "twoFactorDesc": "Thêm một lớp bảo mật bổ sung cho tài khoản",
- "twoFactorInstructions": "Xác thực hai yếu tố thêm một lớp bảo mật bổ sung. Khi được bật, bạn sẽ cần nhập mã từ ứng dụng xác thực của mình ngoài mật khẩu khi đăng nhập.",
- "manageSettings": "Quản lý cài đặt bảo mật tài khoản",
- "updatePasswordDesc": "Cập nhật mật khẩu để giữ tài khoản an toàn",
- "enterCurrentPassword": "Nhập mật khẩu hiện tại",
- "enterNewPassword": "Nhập mật khẩu mới",
- "confirmNewPassword": "Xác nhận mật khẩu mới",
- "updatePassword": "Cập nhật mật khẩu",
- "manageDevices": "Quản lý các thiết bị đã đăng nhập vào tài khoản",
- "unknownDevice": "Thiết bị không xác định",
- "confirmRevokeAll": "Bạn có chắc chắn muốn thu hồi tất cả các session khác?",
- "revokeAllOtherSessions": "Thu hồi tất cả sessions khác",
- "setup2FA": "Thiết lập Xác thực Hai yếu tố",
- "scanQRCode": "Quét mã QR này bằng ứng dụng xác thực của bạn",
- "qrCodeAlt": "Mã QR cho xác thực hai yếu tố",
- "cantScan": "Không thể quét? Nhập mã này theo cách thủ công",
- "verificationCode": "Mã xác thực",
- "enter6DigitCode": "Nhập mã 6 chữ số",
- "enterCodeFromApp": "Nhập mã 6 chữ số từ ứng dụng xác thực của bạn",
- "verifyAndEnable": "Xác thực & Bật"
- },
- "notifications": {
- "title": "Thông báo",
- "label": "Thông báo"
- },
- "billing": {
- "title": "Thanh toán",
- "label": "Thanh toán"
- },
- "apiKeys": {
- "title": "Khóa API",
- "label": "Khóa API",
- "manageKeys": "Quản lý khóa API để truy cập theo chương trình",
- "yourApiKeys": "Khóa API của bạn",
- "createAndManage": "Tạo và quản lý khóa API để truy cập API",
- "createApiKey": "Tạo khóa API",
- "noApiKeys": "Chưa có khóa API nào",
- "createFirstKey": "Tạo khóa API đầu tiên để bắt đầu",
- "name": "Tên",
- "nameRequired": "Tên là bắt buộc",
- "description": "Mô tả",
- "created": "Đã tạo",
- "lastUsed": "Sử dụng lần cuối",
- "expires": "Hết hạn",
- "never": "Không bao giờ",
- "copy": "Sao chép",
- "copied": "Đã sao chép",
- "delete": "Xóa",
- "show": "Hiện",
- "hide": "Ẩn",
- "failedToLoad": "Không thể tải API keys",
- "failedToCreate": "Không thể tạo API key",
- "failedToDelete": "Không thể xóa API key",
- "deletedSuccessfully": "API key đã được xóa thành công",
- "confirmDelete": "Bạn có chắc chắn muốn xóa \"{name}\"? Hành động này không thể hoàn tác.",
- "newApiKeyCreated": "Đã tạo khóa API mới",
- "saveKeySecurely": "Hãy chắc chắn sao chép khóa API của bạn ngay bây giờ. Bạn sẽ không thể xem lại nó!",
- "keyCopied": "Đã sao chép API key vào clipboard",
- "namePlaceholder": "VD: Production Key, Development Key",
- "nameHelper": "Tên mô tả cho khóa API này",
- "descriptionPlaceholder": "Mô tả tùy chọn cho khóa API này",
- "newKeyFor": "Khóa API mới cho \"{name}\"",
- "important": "Quan trọng",
- "securityBestPractices": "Thực hành bảo mật tốt nhất",
- "practice1": "Giữ khóa API của bạn an toàn và không bao giờ chia sẻ công khai",
- "practice2": "Sử dụng biến môi trường hoặc công cụ quản lý bí mật an toàn",
- "practice3": "Xoay khóa API thường xuyên",
- "practice4": "Xóa khóa API không sử dụng ngay lập tức",
- "practice5": "Nếu khóa bị xâm phạm, hãy thu hồi ngay lập tức và tạo khóa mới",
- "createForAccess": "Tạo khóa API mới để truy cập theo chương trình",
- "copyApiKey": "Sao chép khóa API",
- "iveCopied": "Tôi đã sao chép khóa"
- }
- },
- "validation": {
- "required": "Trường này là bắt buộc",
- "email": "Định dạng email không hợp lệ",
- "emailRequired": "Email là bắt buộc",
- "password": "Mật khẩu là bắt buộc",
- "passwordMin": "Mật khẩu phải có ít nhất 8 ký tự",
- "passwordConfirm": "Mật khẩu không khớp",
- "passwordConfirmRequired": "Vui lòng xác nhận mật khẩu",
- "minLength": "Phải có ít nhất {min} ký tự",
- "maxLength": "Phải có tối đa {max} ký tự",
- "invalidFormat": "Định dạng không hợp lệ",
- "fullNameRequired": "Họ tên là bắt buộc",
- "fullNameMin": "Họ tên phải có ít nhất 2 ký tự",
- "fullNameMax": "Họ tên phải ít hơn 100 ký tự",
- "passwordUppercase": "Mật khẩu phải chứa ít nhất một chữ hoa",
- "passwordLowercase": "Mật khẩu phải chứa ít nhất một chữ thường",
- "passwordNumber": "Mật khẩu phải chứa ít nhất một số",
- "passwordSpecial": "Mật khẩu phải chứa ít nhất một ký tự đặc biệt",
- "termsRequired": "Bạn phải chấp nhận điều khoản và điều kiện"
- },
- "errors": {
- "generic": "Đã xảy ra lỗi",
- "networkError": "Lỗi mạng. Vui lòng kiểm tra kết nối của bạn.",
- "unauthorized": "Bạn không có quyền thực hiện hành động này",
- "notFound": "Không tìm thấy tài nguyên",
- "serverError": "Lỗi máy chủ. Vui lòng thử lại sau.",
- "unknown": "Đã xảy ra lỗi không xác định"
- },
- "home": {
- "title": "Nền tảng GoodGo",
- "description": "Trải nghiệm thế hệ tiếp theo của trí tuệ tối giản.",
- "welcome": "Chào mừng, {email}!",
- "role": "Vai trò: {role}",
- "pleaseLogin": "Vui lòng đăng nhập để tiếp tục."
- },
- "account": {
- "title": "Tài khoản của tôi",
- "description": "Quản lý cài đặt tài khoản, xem thống kê và theo dõi hoạt động",
- "editProfile": "Chỉnh sửa hồ sơ",
- "verified": "Đã xác thực",
- "stats": {
- "totalChats": "Tổng số Chat",
- "activeSessions": "Phiên đang hoạt động",
- "apiCalls": "Lượt gọi API (Tháng này)",
- "uptime": "Thời gian hoạt động",
- "vsLastMonth": "so với tháng trước"
- },
- "quickActions": "Hành động nhanh",
- "quickActionsDescription": "Các tác vụ quản lý tài khoản thường dùng",
- "actions": {
- "updateProfile": "Cập nhật hồ sơ",
- "manageApiKeys": "Quản lý khóa API",
- "billing": "Thanh toán & Gói đăng ký",
- "notifications": "Cài đặt thông báo"
- },
- "info": {
- "title": "Thông tin tài khoản",
- "description": "Chi tiết tài khoản và trạng thái xác thực",
- "email": "Email",
- "username": "Tên người dùng",
- "memberSince": "Thành viên từ",
- "plan": "Gói"
- },
- "recentActivity": "Hoạt động gần đây",
- "recentActivityDescription": "Các hoạt động và thay đổi tài khoản gần đây",
- "viewAllActivity": "Xem tất cả hoạt động",
- "activity": {
- "apiKeyCreated": "Đã tạo khóa API mới",
- "profileUpdated": "Đã cập nhật thông tin hồ sơ",
- "chatCreated": "Đã bắt đầu cuộc trò chuyện mới",
- "settingsChanged": "Đã thay đổi tùy chọn thông báo"
- },
- "security": {
- "title": "Trạng thái bảo mật",
- "description": "Cài đặt bảo mật tài khoản và khuyến nghị",
- "score": "Điểm bảo mật",
- "excellent": "Tuyệt vời! Tài khoản của bạn được bảo mật tốt.",
- "improve": "Hoàn thành các mục còn lại để cải thiện bảo mật.",
- "emailVerified": "Email đã xác thực",
- "twoFactorEnabled": "Xác thực hai yếu tố",
- "phoneVerified": "Số điện thoại đã xác thực",
- "recoveryCodes": "Mã khôi phục đã tạo",
- "enabled": "Đã bật",
- "enable": "Bật"
- }
- },
- "footer": {
- "tagline": "Xây dựng, triển khai và mở rộng microservices một cách tự tin. Nền tảng cấp doanh nghiệp cho các nhóm phát triển hiện đại.",
- "product": "Sản phẩm",
- "company": "Công ty",
- "support": "Hỗ trợ",
- "features": "Tính năng",
- "pricing": "Bảng giá",
- "documentation": "Tài liệu",
- "about": "Giới thiệu",
- "blog": "Blog",
- "careers": "Tuyển dụng",
- "helpCenter": "Trung tâm trợ giúp",
- "contact": "Liên hệ",
- "status": "Trạng thái",
- "privacyPolicy": "Chính sách bảo mật",
- "termsOfService": "Điều khoản dịch vụ",
- "copyright": "© {year} GoodGo Platform. Bảo lưu mọi quyền.",
- "followUs": "Theo dõi chúng tôi"
- }
-}
\ No newline at end of file
diff --git a/apps/web-client/src/features/shared/lib/brand-constants.ts b/apps/web-client/src/features/shared/lib/brand-constants.ts
deleted file mode 100644
index 00cc571f..00000000
--- a/apps/web-client/src/features/shared/lib/brand-constants.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * EN: Brand Constants - Easy access to brand values
- * VI: Hằng số thương hiệu - Truy cập dễ dàng các giá trị thương hiệu
- *
- * This file provides type-safe constants for brand colors, fonts, and assets.
- * Use these instead of hardcoding values for consistency and maintainability.
- *
- * File này cung cấp constants type-safe cho màu sắc, fonts và assets thương hiệu.
- * Sử dụng các constants này thay vì hardcode giá trị để đảm bảo tính nhất quán và dễ bảo trì.
- */
-
-export const BRAND = {
- /** EN: Brand name and tagline / VI: Tên thương hiệu và slogan */
- name: 'GoodGo Platform',
- tagline: 'Enterprise Microservices Platform',
- description: 'Build, deploy, and scale microservices with confidence',
-
- /**
- * EN: Brand color palette - Use these for consistent branding
- * VI: Bảng màu thương hiệu - Sử dụng để đảm bảo tính nhất quán
- */
- colors: {
- /** Primary brand color - Main identity color (Blue) */
- primary: {
- main: 'var(--brand-primary)',
- light: 'var(--brand-primary-light)',
- dark: 'var(--brand-primary-dark)',
- contrast: 'var(--brand-primary-contrast)',
- hex: '#3B82F6', // For use outside CSS
- },
- /** Secondary brand color - Supporting color (Purple) */
- secondary: {
- main: 'var(--brand-secondary)',
- light: 'var(--brand-secondary-light)',
- dark: 'var(--brand-secondary-dark)',
- hex: '#8B5CF6',
- },
- /** Accent color - Call-to-action color (Cyan) */
- accent: {
- main: 'var(--brand-accent)',
- hex: '#06B6D4',
- },
- /** Brand gradients - For backgrounds and special elements */
- gradients: {
- primary: 'var(--brand-gradient-primary)',
- accent: 'var(--brand-gradient-accent)',
- },
- },
-
- /**
- * EN: Typography system - Font families for different use cases
- * VI: Hệ thống typography - Font families cho các trường hợp khác nhau
- */
- fonts: {
- display: 'var(--font-display)', // For hero titles (48px+)
- heading: 'var(--font-heading)', // For section headings (24-36px)
- body: 'var(--font-sans)', // For body text (16px)
- mono: 'var(--font-mono)', // For code blocks
- },
-
- /**
- * EN: Brand assets paths - Logo, icons, illustrations
- * VI: Đường dẫn brand assets - Logo, icons, illustrations
- */
- assets: {
- logo: {
- full: '/brand-assets/logo/logo-full.svg',
- icon: '/brand-assets/logo/logo-icon.svg',
- wordmark: '/brand-assets/logo/logo-wordmark.svg',
- },
- icons: {
- favicon: '/brand-assets/icons/favicon.svg',
- },
- illustrations: {
- empty: '/brand-assets/illustrations/empty-state.svg',
- error: '/brand-assets/illustrations/error-state.svg',
- },
- },
-} as const;
-
-/**
- * EN: Type-safe brand color getter
- * VI: Hàm lấy màu thương hiệu type-safe
- *
- * @example
- * ```tsx
- * const primaryColor = getBrandColor('primary.main');
- * const secondaryHex = getBrandColor('secondary.hex');
- * ```
- */
-export const getBrandColor = (path: string): string => {
- const keys = path.split('.');
- let value: any = BRAND.colors;
-
- for (const key of keys) {
- value = value?.[key];
- }
-
- return value || '';
-};
-
-/**
- * EN: Brand logo path getter
- * VI: Hàm lấy đường dẫn logo thương hiệu
- *
- * @param variant - Logo variant: 'full' | 'icon' | 'wordmark'
- * @returns Logo file path
- */
-export const getBrandLogo = (variant: 'full' | 'icon' | 'wordmark' = 'full'): string => {
- return BRAND.assets.logo[variant];
-};
-
-/**
- * EN: Brand illustration path getter
- * VI: Hàm lấy đường dẫn illustration thương hiệu
- *
- * @param type - Illustration type: 'empty' | 'error'
- * @returns Illustration file path
- */
-export const getBrandIllustration = (type: 'empty' | 'error'): string => {
- return BRAND.assets.illustrations[type];
-};
diff --git a/apps/web-client/src/features/shared/lib/index.ts b/apps/web-client/src/features/shared/lib/index.ts
deleted file mode 100644
index 9be2f312..00000000
--- a/apps/web-client/src/features/shared/lib/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * EN: Shared library utilities exports
- * VI: Xuất các utility thư viện dùng chung
- */
-
-export * from './utils';
-export * from './brand-constants';
\ No newline at end of file
diff --git a/apps/web-client/src/features/shared/lib/utils.ts b/apps/web-client/src/features/shared/lib/utils.ts
deleted file mode 100644
index 7ebd5c56..00000000
--- a/apps/web-client/src/features/shared/lib/utils.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type ClassValue, clsx } from 'clsx';
-
-/**
- * EN: Utility function to merge class names with conditional logic
- * VI: Hàm tiện ích để hợp nhất tên class với logic có điều kiện
- *
- * @param inputs - Class names or conditional class objects / Tên class hoặc object class có điều kiện
- * @returns Merged class string / Chuỗi class đã được hợp nhất
- */
-export function cn(...inputs: ClassValue[]) {
- return clsx(inputs);
-}
diff --git a/apps/web-client/src/features/shared/middleware/auth-guard.tsx b/apps/web-client/src/features/shared/middleware/auth-guard.tsx
deleted file mode 100644
index 5cfd5ab6..00000000
--- a/apps/web-client/src/features/shared/middleware/auth-guard.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-'use client';
-
-import React, { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
-import { useAuthStore } from '../../../stores/auth-store';
-import { Role } from '@goodgo/types';
-import { Card } from '../components/ui/card';
-import { Button } from '../components/ui/button';
-
-/**
- * EN: Auth Guard Props
- * VI: Props cho Auth Guard
- */
-interface AuthGuardProps {
- /** EN: Content to render when authenticated / VI: Nội dung để render khi đã xác thực */
- children: React.ReactNode;
- /** EN: Required authentication / VI: Yêu cầu xác thực */
- requireAuth?: boolean;
- /** EN: Required roles for access / VI: Vai trò yêu cầu để truy cập */
- requiredRoles?: Role[];
- /** EN: Redirect path when not authenticated / VI: Đường dẫn redirect khi chưa xác thực */
- redirectTo?: string;
- /** EN: Fallback component while checking auth / VI: Component fallback trong khi kiểm tra auth */
- fallback?: React.ReactNode;
- /** EN: Whether to check on client side only / VI: Có chỉ kiểm tra ở client side không */
- clientOnly?: boolean;
-}
-
-/**
- * EN: Auth Guard Component - Protects routes based on authentication and role requirements
- * VI: Auth Guard Component - Bảo vệ routes dựa trên xác thực và yêu cầu vai trò
- *
- * Features:
- * - Client-side authentication checking
- * - Role-based access control
- * - Automatic redirects
- * - Loading states
- * - Custom fallback components
- *
- * @example
- * ```tsx
- * // Require authentication
- *
- *
- *
- *
- * // Require specific role
- *
- *
- *
- *
- * // Require multiple roles
- *
- *
- *
- * ```
- */
-export function AuthGuard({
- children,
- requireAuth = true,
- requiredRoles = [],
- redirectTo,
- fallback,
- clientOnly = true,
-}: AuthGuardProps) {
- const router = useRouter();
- const { user, isAuthenticated, isLoading, fetchUser } = useAuthStore();
- const [isChecking, setIsChecking] = useState(true);
-
- useEffect(() => {
- // If clientOnly is true, skip server-side checks
- if (clientOnly) {
- setIsChecking(false);
- return;
- }
-
- // Fetch user data if not loaded
- if (!user && !isLoading) {
- fetchUser().finally(() => setIsChecking(false));
- } else {
- setIsChecking(false);
- }
- }, [user, isLoading, fetchUser, clientOnly]);
-
- // Show loading state
- if (isChecking || isLoading) {
- if (fallback) {
- return <>{fallback}>;
- }
-
- return (
-
- );
- }
-
- // Check authentication requirement
- if (requireAuth && !isAuthenticated) {
- // Redirect to login or custom path
- const redirectPath = redirectTo || '/auth/login';
- if (typeof window !== 'undefined') {
- router.push(redirectPath);
- }
- return null;
- }
-
- // Check role requirements
- if (requiredRoles.length > 0 && user) {
- const hasRequiredRole = requiredRoles.includes(user.role);
- if (!hasRequiredRole) {
- // Show access denied
- return (
-
-
- Access Denied
-
- You don't have permission to access this resource.
-
-
- router.back()}>
- Go Back
-
- router.push('/dashboard')}>
- Dashboard
-
-
-
-
- );
- }
- }
-
- // All checks passed, render children
- return <>{children}>;
-}
-
-/**
- * EN: Require Auth HOC - Higher-order component for authentication
- * VI: Require Auth HOC - Higher-order component cho xác thực
- *
- * @example
- * ```tsx
- * const ProtectedPage = requireAuth(MyComponent);
- * const AdminPage = requireAuth(MyComponent, [Role.ADMIN]);
- * ```
- */
-export function requireAuth(
- Component: React.ComponentType
,
- requiredRoles?: Role[]
-) {
- return function AuthenticatedComponent(props: P) {
- return (
-
-
-
- );
- };
-}
-
-/**
- * EN: Role-based guard components
- * VI: Components guard dựa trên vai trò
- */
-export const RequireAdmin: React.FC<{ children: React.ReactNode }> = ({ children }) => (
-
- {children}
-
-);
-
-export const RequireSuperAdmin: React.FC<{ children: React.ReactNode }> = ({ children }) => (
-
- {children}
-
-);
-
-/**
- * EN: Utility functions for role checking
- * VI: Hàm utility để kiểm tra vai trò
- */
-export const hasRole = (userRole: Role | undefined, requiredRoles: Role[]): boolean => {
- if (!userRole) return false;
- return requiredRoles.includes(userRole);
-};
-
-export const hasAdminRole = (userRole: Role | undefined): boolean => {
- return hasRole(userRole, [Role.ADMIN, Role.SUPER_ADMIN]);
-};
-
-export const hasSuperAdminRole = (userRole: Role | undefined): boolean => {
- return hasRole(userRole, [Role.SUPER_ADMIN]);
-};
-
-export const canManageUsers = (userRole: Role | undefined): boolean => {
- return hasRole(userRole, [Role.ADMIN, Role.SUPER_ADMIN]);
-};
-
-export const canDeleteUsers = (currentUserRole: Role | undefined, targetUserRole: Role): boolean => {
- if (!currentUserRole) return false;
-
- // Super admin can delete anyone
- if (currentUserRole === Role.SUPER_ADMIN) return true;
-
- // Admin can delete users but not other admins or super admins
- if (currentUserRole === Role.ADMIN) {
- return targetUserRole === Role.USER;
- }
-
- // Users cannot delete anyone
- return false;
-};
\ No newline at end of file
diff --git a/apps/web-client/src/features/shared/utils/cn.ts b/apps/web-client/src/features/shared/utils/cn.ts
deleted file mode 100644
index 4d5c655d..00000000
--- a/apps/web-client/src/features/shared/utils/cn.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * EN: className utility function
- * VI: Hàm tiện ích cho className
- *
- * A utility function to merge class names using clsx.
- * Hàm tiện ích để merge class names sử dụng clsx.
- */
-
-import { clsx, type ClassValue } from 'clsx';
-
-/**
- * Merge multiple class names into a single string
- * @param inputs - Class names to merge
- * @returns Merged class name string
- */
-export function cn(...inputs: ClassValue[]) {
- return clsx(inputs);
-}
diff --git a/apps/web-client/src/features/shared/utils/index.ts b/apps/web-client/src/features/shared/utils/index.ts
deleted file mode 100644
index 8eeeda07..00000000
--- a/apps/web-client/src/features/shared/utils/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * EN: Shared Utilities
- * VI: Các Utilities dùng chung
- *
- * This file exports all shared utility functions.
- * File này export tất cả các utility functions dùng chung.
- */
-
-export * from './cn';
diff --git a/apps/web-client/src/features/theme/components/index.ts b/apps/web-client/src/features/theme/components/index.ts
deleted file mode 100644
index 7f1ad859..00000000
--- a/apps/web-client/src/features/theme/components/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * EN: Theme components exports
- * VI: Xuất các component theme
- */
-
-export * from './theme-toggle-enhanced';
-export * from './language-switcher';
\ No newline at end of file
diff --git a/apps/web-client/src/features/theme/components/language-switcher.tsx b/apps/web-client/src/features/theme/components/language-switcher.tsx
deleted file mode 100644
index 22cdb8dc..00000000
--- a/apps/web-client/src/features/theme/components/language-switcher.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-'use client';
-
-import React from 'react';
-import { Button } from '@/features/shared/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem
-} from '@/features/shared/components/ui/dropdown-menu';
-import { cn } from '@/shared/lib/utils';
-import { useI18n } from '../i18n-context';
-
-/**
- * EN: Language configuration
- * VI: Cấu hình ngôn ngữ
- */
-const languages = [
- { code: 'en', label: 'English', nativeName: 'English' },
- { code: 'vi', label: 'Tiếng Việt', nativeName: 'Tiếng Việt' },
-] as const;
-
-export type LanguageCode = typeof languages[number]['code'];
-
-/**
- * EN: Language Switcher - Multi-language support
- * VI: Bộ chuyển ngôn ngữ - Hỗ trợ đa ngôn ngữ
- *
- * Features:
- * - Dropdown menu for language selection
- * - Persists locale to localStorage via context
- * - Smooth language switching without page reload
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function LanguageSwitcher() {
- const { locale: currentLocale, setLocale } = useI18n();
-
- const currentLanguage = languages.find(lang => lang.code === currentLocale) || languages[0];
-
- /**
- * EN: Handle language change
- * VI: Xử lý thay đổi ngôn ngữ
- */
- const handleLanguageChange = (newLocale: LanguageCode) => {
- setLocale(newLocale as any);
- };
-
- return (
-
-
-
- {currentLanguage.code.toUpperCase()}
-
-
-
-
- {languages.map((language) => {
- const isActive = currentLocale === language.code;
-
- return (
- handleLanguageChange(language.code)}
- className={cn(
- 'flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors',
- 'hover:bg-bg-tertiary focus:bg-bg-tertiary',
- isActive && 'bg-brand-primary/10 text-brand-primary'
- )}
- >
-
- {language.nativeName}
- {language.label}
-
- {isActive && (
- ✓
- )}
-
- );
- })}
-
-
- );
-}
-
-/**
- * EN: Compact Language Switcher - Shows only code
- * VI: Language Switcher nhỏ gọn - Chỉ hiển thị mã
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function LanguageSwitcherCompact() {
- const { locale: currentLocale, setLocale } = useI18n();
- const currentLanguage = languages.find(lang => lang.code === currentLocale) || languages[0];
-
- const toggleLanguage = () => {
- const newLocale = currentLocale === 'en' ? 'vi' : 'en';
- setLocale(newLocale);
- };
-
- return (
-
-
- {currentLanguage.code.toUpperCase()}
-
-
- );
-}
diff --git a/apps/web-client/src/features/theme/components/theme-toggle-enhanced.tsx b/apps/web-client/src/features/theme/components/theme-toggle-enhanced.tsx
deleted file mode 100644
index 3438ba37..00000000
--- a/apps/web-client/src/features/theme/components/theme-toggle-enhanced.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-'use client';
-
-import React, { useState, useEffect } from 'react';
-import { useTheme } from '@/features/theme';
-import { Sun, Moon, Monitor } from 'lucide-react';
-import { Button } from '@/features/shared/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem
-} from '@/features/shared/components/ui/dropdown-menu';
-import { cn } from '@/shared/lib/utils';
-
-/**
- * EN: Enhanced Theme Toggle - Professional theme switcher with brand styling
- * VI: Theme Toggle nâng cao - Bộ chuyển theme chuyên nghiệp với brand styling
- *
- * Features:
- * - Dropdown menu with 3 options: Light, Dark, System
- * - Glassmorphism styling
- * - Icons from lucide-react
- * - Smooth transitions
- * - Keyboard accessible
- * - Prevents hydration mismatch
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function ThemeToggle() {
- const { theme, setTheme, resolvedTheme } = useTheme();
-
- // EN: Track mounted state to prevent hydration mismatch
- // VI: Theo dõi trạng thái mounted để tránh hydration mismatch
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- }, []);
-
- const themeOptions = [
- { value: 'light' as const, label: 'Light', icon: Sun },
- { value: 'dark' as const, label: 'Dark', icon: Moon },
- { value: 'system' as const, label: 'System', icon: Monitor },
- ];
-
- // EN: Use Monitor icon during SSR to prevent mismatch
- // VI: Sử dụng icon Monitor trong SSR để tránh mismatch
- const currentIcon = !mounted ? Monitor : (resolvedTheme === 'dark' ? Moon : Sun);
- const CurrentIcon = currentIcon;
-
- return (
-
-
-
-
-
-
-
-
- {themeOptions.map((option) => {
- const Icon = option.icon;
- const isActive = theme === option.value;
-
- return (
- setTheme(option.value)}
- className={cn(
- 'flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors',
- 'hover:bg-bg-tertiary focus:bg-bg-tertiary',
- isActive && 'bg-brand-primary/10 text-brand-primary'
- )}
- >
-
- {option.label}
- {isActive && (
- ✓
- )}
-
- );
- })}
-
-
- );
-}
-
-/**
- * EN: Simple Theme Toggle Button - Toggles between light and dark only
- * VI: Button Toggle Theme đơn giản - Chỉ chuyển đổi giữa light và dark
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function ThemeToggleButton() {
- const { toggleTheme, resolvedTheme } = useTheme();
-
- // EN: Track mounted state to prevent hydration mismatch
- // VI: Theo dõi trạng thái mounted để tránh hydration mismatch
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- }, []);
-
- // EN: Use Monitor icon during SSR to prevent mismatch
- // VI: Sử dụng icon Monitor trong SSR để tránh mismatch
- const Icon = !mounted ? Monitor : (resolvedTheme === 'dark' ? Sun : Moon);
-
- return (
-
-
-
- );
-}
diff --git a/apps/web-client/src/features/theme/components/theme-toggle.tsx b/apps/web-client/src/features/theme/components/theme-toggle.tsx
deleted file mode 100644
index c054ac2e..00000000
--- a/apps/web-client/src/features/theme/components/theme-toggle.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-'use client';
-
-import React from 'react';
-import { useTheme } from '@/features/theme';
-import { Sun, Moon, Monitor } from 'lucide-react';
-import { Button } from '@/features/shared/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem
-} from '@/features/shared/components/ui/dropdown-menu';
-import { cn } from '@/shared/lib/utils';
-
-/**
- * EN: Enhanced Theme Toggle - Professional theme switcher with brand styling
- * VI: Theme Toggle nâng cao - Bộ chuyển theme chuyên nghiệp với brand styling
- *
- * Features:
- * - Dropdown menu with 3 options: Light, Dark, System
- * - Glassmorphism styling
- * - Icons from lucide-react
- * - Smooth transitions
- * - Keyboard accessible
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function ThemeToggle() {
- const { theme, setTheme, resolvedTheme } = useTheme();
-
- const themeOptions = [
- { value: 'light' as const, label: 'Light', icon: Sun },
- { value: 'dark' as const, label: 'Dark', icon: Moon },
- { value: 'system' as const, label: 'System', icon: Monitor },
- ];
-
- const currentIcon = resolvedTheme === 'dark' ? Moon : Sun;
- const CurrentIcon = currentIcon;
-
- return (
-
-
-
-
-
-
-
-
- {themeOptions.map((option) => {
- const Icon = option.icon;
- const isActive = theme === option.value;
-
- return (
- setTheme(option.value)}
- className={cn(
- 'flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors',
- 'hover:bg-bg-tertiary focus:bg-bg-tertiary',
- isActive && 'bg-brand-primary/10 text-brand-primary'
- )}
- >
-
- {option.label}
- {isActive && (
- ✓
- )}
-
- );
- })}
-
-
- );
-}
-
-/**
- * EN: Simple Theme Toggle Button - Toggles between light and dark only
- * VI: Button Toggle Theme đơn giản - Chỉ chuyển đổi giữa light và dark
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function ThemeToggleButton() {
- const { toggleTheme, resolvedTheme } = useTheme();
- const Icon = resolvedTheme === 'dark' ? Sun : Moon;
-
- return (
-
-
-
- );
-}
diff --git a/apps/web-client/src/features/theme/hooks/use-theme.ts b/apps/web-client/src/features/theme/hooks/use-theme.ts
deleted file mode 100644
index 29788c03..00000000
--- a/apps/web-client/src/features/theme/hooks/use-theme.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * EN: Theme hook - Re-export from theme context
- * VI: Hook theme - Re-export từ theme context
- */
-
-export * from '../theme-context';
\ No newline at end of file
diff --git a/apps/web-client/src/features/theme/i18n-config.ts b/apps/web-client/src/features/theme/i18n-config.ts
deleted file mode 100644
index d9b0d01f..00000000
--- a/apps/web-client/src/features/theme/i18n-config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * EN: i18n configuration for next-intl
- * VI: Cấu hình i18n cho next-intl
- */
-
-/**
- * EN: Supported locales
- * VI: Các ngôn ngữ được hỗ trợ
- */
-export const locales = ['en', 'vi'] as const;
-
-/**
- * EN: Default locale
- * VI: Ngôn ngữ mặc định
- */
-export const defaultLocale = 'en' as const;
-
-/**
- * EN: Default timezone for consistent date/time formatting
- * VI: Timezone mặc định để format date/time nhất quán
- */
-export const defaultTimeZone = 'UTC' as const;
-
-/**
- * EN: Locale type
- * VI: Kiểu locale
- */
-export type Locale = (typeof locales)[number];
-
-/**
- * EN: Check if a string is a valid locale
- * VI: Kiểm tra xem một chuỗi có phải là locale hợp lệ không
- */
-export function isValidLocale(locale: string): locale is Locale {
- return locales.includes(locale as Locale);
-}
diff --git a/apps/web-client/src/features/theme/i18n-context.tsx b/apps/web-client/src/features/theme/i18n-context.tsx
deleted file mode 100644
index 9af4df87..00000000
--- a/apps/web-client/src/features/theme/i18n-context.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-/**
- * EN: i18n Context for managing locale state
- * VI: Context i18n để quản lý trạng thái locale
- */
-
-import * as React from 'react';
-import { type Locale, defaultLocale, isValidLocale } from './i18n-config';
-
-/**
- * EN: i18n Context interface
- * VI: Interface cho i18n Context
- */
-interface I18nContextType {
- /**
- * EN: Current locale / VI: Locale hiện tại
- */
- locale: Locale;
- /**
- * EN: Set locale function / VI: Hàm đặt locale
- */
- setLocale: (locale: Locale) => void;
- /**
- * EN: Get locale function / VI: Hàm lấy locale
- */
- getLocale: () => Locale;
-}
-
-/**
- * EN: i18n Context
- * VI: Context i18n
- */
-export const I18nContext = React.createContext(undefined);
-
-
-
-/**
- * EN: Hook to use i18n context
- * VI: Hook để sử dụng i18n context
- */
-export function useI18n() {
- const context = React.useContext(I18nContext);
- if (context === undefined) {
- throw new Error('useI18n must be used within I18nProvider');
- }
- return context;
-}
diff --git a/apps/web-client/src/features/theme/i18n-provider.tsx b/apps/web-client/src/features/theme/i18n-provider.tsx
deleted file mode 100644
index 907a171d..00000000
--- a/apps/web-client/src/features/theme/i18n-provider.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-'use client';
-
-/**
- * EN: I18n Provider wrapper component
- * VI: Component wrapper I18n Provider
- *
- * This component wraps the next-intl provider with our custom context
- */
-
-import { NextIntlClientProvider } from 'next-intl';
-import { I18nContext } from './i18n-context';
-import { defaultTimeZone } from './i18n-config';
-import * as React from 'react';
-import enMessages from '../shared/i18n/en.json';
-import viMessages from '../shared/i18n/vi.json';
-
-/**
- * EN: Get locale from localStorage or browser (duplicate from i18n-context to avoid circular dependency)
- * VI: Lấy locale từ localStorage hoặc browser (duplicate từ i18n-context để tránh circular dependency)
- *
- * @param skipBrowserDetection - Skip browser language detection (for hydration)
- */
-function getStoredLocale(skipBrowserDetection: boolean = false): 'en' | 'vi' {
- if (typeof window === 'undefined') {
- return 'en'; // defaultLocale
- }
-
- // EN: Try to get from localStorage preferences / VI: Thử lấy từ localStorage preferences
- try {
- const stored = localStorage.getItem('preferences');
- if (stored) {
- const parsed = JSON.parse(stored);
- if (parsed.language && (parsed.language === 'en' || parsed.language === 'vi')) {
- return parsed.language;
- }
- }
- } catch {
- // EN: Invalid stored data, continue to browser detection / VI: Dữ liệu lưu không hợp lệ, tiếp tục detect browser
- }
-
- // EN: Skip browser detection during hydration to prevent mismatch
- // VI: Bỏ qua browser detection trong hydration để tránh mismatch
- if (skipBrowserDetection) {
- return 'en'; // defaultLocale
- }
-
- // EN: Detect from browser language / VI: Phát hiện từ ngôn ngữ browser
- if (typeof navigator !== 'undefined') {
- const browserLang = navigator.language || navigator.languages?.[0] || '';
- const langCode = browserLang.split('-')[0].toLowerCase();
- if (langCode === 'vi') {
- return 'vi';
- }
- }
-
- return 'en'; // defaultLocale
-}
-
-/**
- * EN: Main I18n Provider component
- * VI: Component I18n Provider chính
- */
-export function I18nProvider({ children }: { children: React.ReactNode }) {
- // EN: Track if component has mounted (hydrated) to prevent hydration mismatch
- // VI: Theo dõi nếu component đã mount (hydrated) để tránh hydration mismatch
- const [mounted, setMounted] = React.useState(false);
-
- // EN: Initialize with default locale to prevent hydration mismatch
- // VI: Khởi tạo với locale mặc định để tránh hydration mismatch
- // Server always renders with defaultLocale, so client must start with same value
- const [locale, setLocaleState] = React.useState<'en' | 'vi'>('en');
-
- /**
- * EN: Set locale and persist to localStorage
- * VI: Đặt locale và lưu vào localStorage
- */
- const setLocale = React.useCallback((newLocale: 'en' | 'vi') => {
- if (newLocale !== 'en' && newLocale !== 'vi') {
- console.warn(`Invalid locale: ${newLocale}`);
- return;
- }
-
- setLocaleState(newLocale);
-
- // EN: Update localStorage preferences / VI: Cập nhật localStorage preferences
- if (typeof window !== 'undefined') {
- try {
- const stored = localStorage.getItem('preferences');
- const preferences = stored ? JSON.parse(stored) : {};
- preferences.language = newLocale;
- localStorage.setItem('preferences', JSON.stringify(preferences));
- } catch (error) {
- console.error('Failed to save locale preference:', error);
- }
-
- // EN: Update HTML lang attribute / VI: Cập nhật thuộc tính lang của HTML
- document.documentElement.lang = newLocale;
- }
- }, []);
-
- /**
- * EN: Get current locale
- * VI: Lấy locale hiện tại
- */
- const getLocale = React.useCallback(() => {
- return locale;
- }, [locale]);
-
- // EN: Mark as mounted AFTER hydration (no auto-detection, keep default 'en')
- // VI: Đánh dấu đã mount SAU khi hydration (không tự động phát hiện, giữ mặc định 'en')
- React.useEffect(() => {
- setMounted(true);
- // EN: Check ONLY localStorage for explicit user choice (no browser detection)
- // VI: Chỉ kiểm tra localStorage cho lựa chọn rõ ràng của người dùng (không phát hiện browser)
- try {
- const stored = localStorage.getItem('preferences');
- if (stored) {
- const parsed = JSON.parse(stored);
- if (parsed.language && (parsed.language === 'en' || parsed.language === 'vi')) {
- setLocaleState(parsed.language);
- }
- }
- } catch {
- // EN: Keep default 'en' if localStorage read fails
- // VI: Giữ mặc định 'en' nếu đọc localStorage thất bại
- }
- }, []); // Run once on mount
-
- // EN: Initialize HTML lang attribute on mount / VI: Khởi tạo thuộc tính lang của HTML khi mount
- React.useEffect(() => {
- if (typeof document !== 'undefined' && mounted) {
- document.documentElement.lang = locale;
- }
- }, [locale, mounted]);
-
- // EN: Get messages based on locale / VI: Lấy messages dựa trên locale
- // EN: Logic to be applied: ensure we use 'en' until mounted to match server
- const activeLocale = mounted ? locale : 'en';
-
- const messages = React.useMemo(() => {
- return activeLocale === 'vi' ? viMessages : enMessages;
- }, [activeLocale]);
-
- const customContextValue = React.useMemo(
- () => ({
- locale: activeLocale,
- setLocale,
- getLocale,
- }),
- [activeLocale, setLocale, getLocale]
- );
-
- // EN: AGGRESSIVE HYDRATION FIX: Don't render translated content until after mount
- // VI: FIX HYDRATION MẠNH: Không render nội dung đã dịch cho đến sau khi mount
- // This prevents ANY hydration mismatch by ensuring both server and client
- // render the same null/skeleton content initially, then client renders
- // the full content with translations after hydration is complete.
- if (!mounted) {
- return (
-
-
- {/*
- EN: Render children with default English during SSR/hydration
- VI: Render children với English mặc định trong SSR/hydration
- This ensures server and client match during hydration.
- After mount, we'll re-render with the user's locale preference.
- */}
- {children}
-
-
- );
- }
-
- return (
-
-
- {children}
-
-
- );
-}
diff --git a/apps/web-client/src/features/theme/index.ts b/apps/web-client/src/features/theme/index.ts
deleted file mode 100644
index 396afdf9..00000000
--- a/apps/web-client/src/features/theme/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * EN: Theme Feature Public API
- * VI: Public API của Feature Theme
- *
- * This file exports the public API of the theme feature.
- * File này export public API của feature theme.
- */
-
-// Components
-export * from './components/theme-toggle-enhanced';
-export * from './components/language-switcher';
-
-// Contexts
-export * from './theme-context';
-export { useI18n } from './i18n-context';
-export { I18nProvider } from './i18n-provider';
-
-// Hooks
-export * from './hooks/use-theme';
-
-// Types
-// export type * from './types/theme.types';
diff --git a/apps/web-client/src/features/theme/theme-context.tsx b/apps/web-client/src/features/theme/theme-context.tsx
deleted file mode 100644
index 1bf76645..00000000
--- a/apps/web-client/src/features/theme/theme-context.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
-
-/**
- * EN: Theme mode type
- * VI: Kiểu chế độ theme
- */
-export type ThemeMode = 'light' | 'dark' | 'system';
-
-/**
- * EN: Theme context value interface
- * VI: Interface giá trị context theme
- */
-interface ThemeContextValue {
- /** EN: Current theme mode / VI: Chế độ theme hiện tại */
- theme: ThemeMode;
- /** EN: Resolved theme (light or dark) / VI: Theme đã được resolve (light hoặc dark) */
- resolvedTheme: 'light' | 'dark';
- /** EN: Set theme mode / VI: Đặt chế độ theme */
- setTheme: (theme: ThemeMode) => void;
- /** EN: Toggle between light and dark / VI: Chuyển đổi giữa light và dark */
- toggleTheme: () => void;
-}
-
-/**
- * EN: Theme context
- * VI: Context theme
- */
-const ThemeContext = createContext(undefined);
-
-/**
- * EN: Get system preference for dark mode
- * VI: Lấy preference hệ thống cho dark mode
- */
-const getSystemTheme = (): 'light' | 'dark' => {
- if (typeof window === 'undefined') return 'dark';
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
-};
-
-/**
- * EN: Apply theme class to document
- * VI: Áp dụng class theme vào document
- */
-const applyTheme = (theme: 'light' | 'dark') => {
- if (typeof window === 'undefined') return;
-
- const root = document.documentElement;
- root.classList.remove('light', 'dark');
- root.classList.add(theme);
- root.setAttribute('data-theme', theme);
-};
-
-/**
- * EN: Theme provider component
- * VI: Component provider theme
- *
- * @param children - Child components / Components con
- */
-export function ThemeProvider({ children }: { children: React.ReactNode }) {
- const [theme, setThemeState] = useState('dark');
- const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
- if (typeof window === 'undefined') return 'dark';
-
- // EN: Load from localStorage or default to dark
- // VI: Load từ localStorage hoặc mặc định là dark
- const stored = localStorage.getItem('theme') as ThemeMode | null;
- if (stored && (stored === 'light' || stored === 'dark' || stored === 'system')) {
- return stored === 'system' ? getSystemTheme() : stored;
- }
- return 'dark';
- });
-
- // EN: Initialize theme from localStorage
- // VI: Khởi tạo theme từ localStorage
- useEffect(() => {
- if (typeof window === 'undefined') return;
-
- const stored = localStorage.getItem('theme') as ThemeMode | null;
- if (stored && (stored === 'light' || stored === 'dark' || stored === 'system')) {
- setThemeState(stored);
- const resolved = stored === 'system' ? getSystemTheme() : stored;
- setResolvedTheme(resolved);
- applyTheme(resolved);
- } else {
- // EN: Default to dark theme
- // VI: Mặc định là dark theme
- setResolvedTheme('dark');
- applyTheme('dark');
- }
- }, []);
-
- // EN: Listen to system theme changes
- // VI: Lắng nghe thay đổi theme hệ thống
- useEffect(() => {
- if (typeof window === 'undefined' || theme !== 'system') return;
-
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
- const handleChange = (e: MediaQueryListEvent) => {
- const newTheme = e.matches ? 'dark' : 'light';
- setResolvedTheme(newTheme);
- applyTheme(newTheme);
- };
-
- mediaQuery.addEventListener('change', handleChange);
- return () => mediaQuery.removeEventListener('change', handleChange);
- }, [theme]);
-
- // EN: Apply theme when resolved theme changes
- // VI: Áp dụng theme khi resolved theme thay đổi
- useEffect(() => {
- applyTheme(resolvedTheme);
- }, [resolvedTheme]);
-
- /**
- * EN: Set theme mode and persist to localStorage
- * VI: Đặt chế độ theme và lưu vào localStorage
- */
- const setTheme = useCallback((newTheme: ThemeMode) => {
- setThemeState(newTheme);
-
- if (typeof window !== 'undefined') {
- localStorage.setItem('theme', newTheme);
- }
-
- const resolved = newTheme === 'system' ? getSystemTheme() : newTheme;
- setResolvedTheme(resolved);
- applyTheme(resolved);
- }, []);
-
- /**
- * EN: Toggle between light and dark themes
- * VI: Chuyển đổi giữa theme light và dark
- */
- const toggleTheme = useCallback(() => {
- const currentResolved = resolvedTheme;
- const newTheme = currentResolved === 'light' ? 'dark' : 'light';
- setTheme(newTheme);
- }, [resolvedTheme, setTheme]);
-
- // EN: Keyboard shortcut: Ctrl+Shift+T to toggle theme
- // VI: Phím tắt: Ctrl+Shift+T để chuyển theme
- useEffect(() => {
- if (typeof window === 'undefined') return;
-
- const handleKeyPress = (e: KeyboardEvent) => {
- if (e.ctrlKey && e.shiftKey && e.key === 'T') {
- e.preventDefault();
- toggleTheme();
- }
- };
-
- window.addEventListener('keydown', handleKeyPress);
- return () => window.removeEventListener('keydown', handleKeyPress);
- }, [toggleTheme]);
-
- const value: ThemeContextValue = {
- theme,
- resolvedTheme,
- setTheme,
- toggleTheme,
- };
-
- return {children} ;
-}
-
-/**
- * EN: Hook to access theme context
- * VI: Hook để truy cập theme context
- *
- * @throws Error if used outside ThemeProvider
- */
-export function useTheme(): ThemeContextValue {
- const context = useContext(ThemeContext);
- if (context === undefined) {
- throw new Error('useTheme must be used within a ThemeProvider / useTheme phải được sử dụng trong ThemeProvider');
- }
- return context;
-}
\ No newline at end of file
diff --git a/apps/web-client/src/lib/api/users.ts b/apps/web-client/src/lib/api/users.ts
deleted file mode 100644
index 64db6cff..00000000
--- a/apps/web-client/src/lib/api/users.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { UserResponse, CreateUserDto, UpdateUserDto, Role } from '@goodgo/types';
-import { apiClient } from '../../services/api/client';
-
-/**
- * EN: Query parameters for users list endpoint
- * VI: Tham số query cho endpoint danh sách users
- */
-export interface GetUsersParams {
- /** EN: Page number for pagination / VI: Số trang cho pagination */
- page?: number;
- /** EN: Number of items per page / VI: Số items mỗi trang */
- limit?: number;
- /** EN: Search query for filtering users / VI: Query tìm kiếm để lọc users */
- search?: string;
- /** EN: Filter by user role / VI: Lọc theo vai trò user */
- role?: Role;
- /** EN: Filter by active status / VI: Lọc theo trạng thái active */
- isActive?: boolean;
- /** EN: Sort field / VI: Trường sắp xếp */
- sortBy?: 'email' | 'createdAt' | 'updatedAt';
- /** EN: Sort direction / VI: Hướng sắp xếp */
- sortOrder?: 'asc' | 'desc';
-}
-
-/**
- * EN: Response structure for paginated users list
- * VI: Cấu trúc response cho danh sách users phân trang
- */
-export interface GetUsersResponse {
- /** EN: Array of user objects / VI: Mảng các objects user */
- data: UserResponse[];
- /** EN: Pagination metadata / VI: Metadata phân trang */
- pagination: {
- /** EN: Current page number / VI: Số trang hiện tại */
- page: number;
- /** EN: Items per page / VI: Items mỗi trang */
- limit: number;
- /** EN: Total number of items / VI: Tổng số items */
- total: number;
- /** EN: Total number of pages / VI: Tổng số trang */
- totalPages: number;
- };
-}
-
-/**
- * EN: Fetch paginated list of users
- * VI: Lấy danh sách users phân trang
- *
- * @param params - Query parameters for filtering and pagination
- * @returns Promise resolving to paginated users response
- */
-export async function getUsers(params: GetUsersParams = {}): Promise {
- const response = await apiClient.get('/users', { params });
- return response.data;
-}
-
-/**
- * EN: Fetch single user by ID
- * VI: Lấy thông tin user theo ID
- *
- * @param id - User ID
- * @returns Promise resolving to user response
- */
-export async function getUser(id: string): Promise {
- const response = await apiClient.get(`/users/${id}`);
- return response.data;
-}
-
-/**
- * EN: Create new user
- * VI: Tạo user mới
- *
- * @param payload - User creation data
- * @returns Promise resolving to created user response
- */
-export async function createUser(payload: CreateUserDto): Promise {
- const response = await apiClient.post('/users', payload);
- return response.data;
-}
-
-/**
- * EN: Update existing user
- * VI: Cập nhật user hiện có
- *
- * @param id - User ID
- * @param payload - User update data
- * @returns Promise resolving to updated user response
- */
-export async function updateUser(id: string, payload: UpdateUserDto): Promise {
- const response = await apiClient.put(`/users/${id}`, payload);
- return response.data;
-}
-
-/**
- * EN: Delete user by ID
- * VI: Xóa user theo ID
- *
- * @param id - User ID
- * @returns Promise resolving when deletion is complete
- */
-export async function deleteUser(id: string): Promise {
- await apiClient.delete(`/users/${id}`);
-}
-
-/**
- * EN: Bulk delete multiple users
- * VI: Xóa nhiều users cùng lúc
- *
- * @param ids - Array of user IDs to delete
- * @returns Promise resolving when bulk deletion is complete
- */
-export async function bulkDeleteUsers(ids: string[]): Promise {
- await apiClient.post('/users/bulk-delete', { ids });
-}
-
-/**
- * EN: Bulk update user roles
- * VI: Cập nhật vai trò cho nhiều users cùng lúc
- *
- * @param updates - Array of user ID and new role pairs
- * @returns Promise resolving when bulk update is complete
- */
-export async function bulkUpdateUserRoles(updates: Array<{ id: string; role: Role }>): Promise {
- await apiClient.post('/users/bulk-update-roles', { updates });
-}
\ No newline at end of file
diff --git a/apps/web-client/src/providers/query-provider.tsx b/apps/web-client/src/providers/query-provider.tsx
deleted file mode 100644
index a5a14031..00000000
--- a/apps/web-client/src/providers/query-provider.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import * as React from 'react';
-
-/**
- * EN: QueryClient configuration with caching
- * VI: Cấu hình QueryClient với caching
- */
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- // EN: Cache time: 5 minutes / VI: Thời gian cache: 5 phút
- staleTime: 5 * 60 * 1000,
- // EN: Cache data for 10 minutes / VI: Cache dữ liệu trong 10 phút
- gcTime: 10 * 60 * 1000,
- // EN: Retry failed requests / VI: Thử lại các request thất bại
- retry: 1,
- // EN: Refetch on window focus / VI: Refetch khi focus window
- refetchOnWindowFocus: false,
- },
- },
-});
-
-/**
- * EN: QueryProvider component - Provides React Query context
- * VI: Component QueryProvider - Cung cấp context React Query
- *
- * Features:
- * - API response caching
- * - Automatic refetching
- * - Error handling
- * - Loading states
- *
- * Tính năng:
- * - Cache phản hồi API
- * - Tự động refetch
- * - Xử lý lỗi
- * - Trạng thái loading
- */
-export function QueryProvider({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/apps/web-client/src/services/api/__tests__/api-keys.api.test.ts b/apps/web-client/src/services/api/__tests__/api-keys.api.test.ts
deleted file mode 100644
index 7fa10a83..00000000
--- a/apps/web-client/src/services/api/__tests__/api-keys.api.test.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { apiKeysApi } from '../api-keys.api';
-import { userApi } from '../user.api';
-
-vi.mock('../user.api', () => ({
- userApi: {
- getProfile: vi.fn(),
- setProfileAttribute: vi.fn(),
- },
-}));
-
-const mockedUserApi = vi.mocked(userApi);
-
-describe('apiKeysApi', () => {
- beforeEach(() => {
- vi.clearAllMocks();
-
- vi.stubGlobal('crypto', {
- getRandomValues: (buffer: Uint8Array) => {
- buffer.fill(1);
- return buffer;
- },
- randomUUID: () => 'uuid-123',
- subtle: {
- digest: vi.fn(async () => new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer),
- },
- });
- });
-
- it('lists active API keys and filters revoked records', async () => {
- mockedUserApi.getProfile.mockResolvedValue({
- success: true,
- data: {
- attributes: [
- {
- key: 'api_key_active',
- value: JSON.stringify({
- id: 'active',
- name: 'Active Key',
- keyPrefix: 'gg_active',
- keySuffix: '1234',
- keyHash: 'hash',
- createdAt: '2026-01-01T00:00:00.000Z',
- lastUsedAt: null,
- expiresAt: null,
- revokedAt: null,
- }),
- valueType: 'Json',
- },
- {
- key: 'api_key_revoked',
- value: JSON.stringify({
- id: 'revoked',
- name: 'Revoked Key',
- keyPrefix: 'gg_revoked',
- keySuffix: '5678',
- keyHash: 'hash',
- createdAt: '2026-01-02T00:00:00.000Z',
- lastUsedAt: null,
- expiresAt: null,
- revokedAt: '2026-01-03T00:00:00.000Z',
- }),
- valueType: 'Json',
- },
- ],
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- const response = await apiKeysApi.list('user-1');
-
- expect(response.success).toBe(true);
- expect(response.data).toHaveLength(1);
- expect(response.data?.[0].id).toBe('active');
- });
-
- it('creates API key and persists description and key record', async () => {
- mockedUserApi.setProfileAttribute.mockResolvedValue({
- success: true,
- data: { key: 'any', value: 'any', valueType: 'String' },
- timestamp: new Date().toISOString(),
- } as never);
-
- const response = await apiKeysApi.create('user-1', {
- name: 'Primary key',
- description: 'Main key for integrations',
- });
-
- expect(response.success).toBe(true);
- expect(response.data?.key.startsWith('gg_')).toBe(true);
- expect(response.data?.apiKey.id).toBe('uuid-123');
- expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledTimes(2);
- expect(mockedUserApi.setProfileAttribute).toHaveBeenNthCalledWith(
- 1,
- 'user-1',
- 'api_key_desc_uuid-123',
- expect.objectContaining({ value: 'Main key for integrations' })
- );
- expect(mockedUserApi.setProfileAttribute).toHaveBeenNthCalledWith(
- 2,
- 'user-1',
- 'api_key_uuid-123',
- expect.objectContaining({ valueType: 'Json' })
- );
- });
-
- it('revokes an existing API key record', async () => {
- mockedUserApi.getProfile.mockResolvedValue({
- success: true,
- data: {
- attributes: [
- {
- key: 'api_key_target',
- value: JSON.stringify({
- id: 'target',
- name: 'Target Key',
- keyPrefix: 'gg_target',
- keySuffix: '9999',
- keyHash: 'hash',
- createdAt: '2026-01-01T00:00:00.000Z',
- lastUsedAt: null,
- expiresAt: null,
- revokedAt: null,
- }),
- valueType: 'Json',
- },
- ],
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- mockedUserApi.setProfileAttribute.mockResolvedValue({
- success: true,
- data: { key: 'api_key_target', value: 'revoked', valueType: 'Json' },
- timestamp: new Date().toISOString(),
- } as never);
-
- const response = await apiKeysApi.delete('user-1', 'target');
-
- expect(response.success).toBe(true);
- expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledTimes(1);
- expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledWith(
- 'user-1',
- 'api_key_target',
- expect.objectContaining({ valueType: 'Json' })
- );
- });
-});
diff --git a/apps/web-client/src/services/api/__tests__/storage.api.test.ts b/apps/web-client/src/services/api/__tests__/storage.api.test.ts
deleted file mode 100644
index 4aa041f0..00000000
--- a/apps/web-client/src/services/api/__tests__/storage.api.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { storageApi } from '../storage.api';
-import { apiClient } from '../client';
-
-vi.mock('../client', () => ({
- apiClient: {
- post: vi.fn(),
- get: vi.fn(),
- },
-}));
-
-const mockedApiClient = vi.mocked(apiClient);
-
-describe('storageApi.uploadAvatar', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('uploads avatar and resolves CDN URL', async () => {
- mockedApiClient.post.mockResolvedValue({
- success: true,
- data: {
- success: true,
- fileId: 'file-1',
- objectKey: 'avatars/file-1.png',
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- mockedApiClient.get.mockResolvedValue({
- success: true,
- data: {
- url: 'https://cdn.goodgo.dev/avatars/file-1.png',
- isCDN: true,
- description: 'cdn',
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
- const response = await storageApi.uploadAvatar(file);
-
- expect(response.success).toBe(true);
- expect(response.data?.fileId).toBe('file-1');
- expect(response.data?.url).toBe('https://cdn.goodgo.dev/avatars/file-1.png');
- });
-
- it('falls back to empty URL when CDN lookup fails', async () => {
- mockedApiClient.post.mockResolvedValue({
- success: true,
- data: {
- success: true,
- fileId: 'file-2',
- objectKey: 'avatars/file-2.png',
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- mockedApiClient.get.mockRejectedValue(new Error('cdn lookup failed'));
-
- const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
- const response = await storageApi.uploadAvatar(file);
-
- expect(response.success).toBe(true);
- expect(response.data?.fileId).toBe('file-2');
- expect(response.data?.url).toBe('');
- });
-
- it('throws when upload API returns unsuccessful payload', async () => {
- mockedApiClient.post.mockResolvedValue({
- success: false,
- data: {
- success: false,
- error: 'Upload failed',
- },
- error: {
- message: 'Upload failed',
- code: 'UPLOAD_FAILED',
- },
- timestamp: new Date().toISOString(),
- } as never);
-
- const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
-
- await expect(storageApi.uploadAvatar(file)).rejects.toThrow('Upload failed');
- });
-});
diff --git a/apps/web-client/src/services/api/api-keys.api.ts b/apps/web-client/src/services/api/api-keys.api.ts
deleted file mode 100644
index 1891cb46..00000000
--- a/apps/web-client/src/services/api/api-keys.api.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { ApiResponse } from '@goodgo/types';
-
-import { userApi } from './user.api';
-
-const API_KEY_ATTRIBUTE_PREFIX = 'api_key_';
-
-/**
- * EN: Public API key metadata exposed to UI.
- * VI: Metadata API key public cho UI.
- */
-export interface ApiKey {
- id: string;
- name: string;
- keyPrefix: string;
- keySuffix: string;
- createdAt: string;
- lastUsedAt: string | null;
- expiresAt: string | null;
-}
-
-interface StoredApiKeyRecord extends ApiKey {
- keyHash: string;
- revokedAt: string | null;
-}
-
-interface CreateApiKeyResult {
- apiKey: ApiKey;
- key: string;
-}
-
-const toAttributeKey = (id: string) => `${API_KEY_ATTRIBUTE_PREFIX}${id}`;
-
-const parseStoredRecord = (value: string): StoredApiKeyRecord | null => {
- try {
- const parsed = JSON.parse(value) as StoredApiKeyRecord;
- if (!parsed?.id || !parsed?.name || !parsed?.keyPrefix || !parsed?.keySuffix) {
- return null;
- }
- return parsed;
- } catch {
- return null;
- }
-};
-
-const createRandomApiKey = (): string => {
- if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
- const randomBuffer = new Uint8Array(24);
- crypto.getRandomValues(randomBuffer);
- const randomPart = Array.from(randomBuffer)
- .map((byte) => byte.toString(16).padStart(2, '0'))
- .join('');
- return `gg_${randomPart}`;
- }
- return `gg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
-};
-
-const hashApiKey = async (apiKey: string): Promise => {
- if (typeof crypto !== 'undefined' && crypto.subtle) {
- const encoded = new TextEncoder().encode(apiKey);
- const hashBuffer = await crypto.subtle.digest('SHA-256', encoded);
- return Array.from(new Uint8Array(hashBuffer))
- .map((byte) => byte.toString(16).padStart(2, '0'))
- .join('');
- }
- return apiKey;
-};
-
-const toPublicApiKey = (record: StoredApiKeyRecord): ApiKey => ({
- id: record.id,
- name: record.name,
- keyPrefix: record.keyPrefix,
- keySuffix: record.keySuffix,
- createdAt: record.createdAt,
- lastUsedAt: record.lastUsedAt,
- expiresAt: record.expiresAt,
-});
-
-/**
- * EN: API keys service persisted via profile attributes.
- * VI: Service API keys lưu trữ qua profile attributes.
- */
-export const apiKeysApi = {
- /**
- * EN: List active API keys.
- * VI: Lấy danh sách API key đang hoạt động.
- */
- list: async (userId: string): Promise> => {
- const profileResponse = await userApi.getProfile(userId);
- if (!profileResponse.success || !profileResponse.data) {
- throw new Error(profileResponse.error?.message || 'Failed to load API keys');
- }
-
- const records = profileResponse.data.attributes
- .filter((attribute) => attribute.key.startsWith(API_KEY_ATTRIBUTE_PREFIX))
- .map((attribute) => parseStoredRecord(attribute.value))
- .filter((record): record is StoredApiKeyRecord => !!record && !record.revokedAt)
- .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
- .map(toPublicApiKey);
-
- return {
- success: true,
- data: records,
- timestamp: new Date().toISOString(),
- };
- },
-
- /**
- * EN: Create API key and return full key once.
- * VI: Tạo API key và trả full key duy nhất một lần.
- */
- create: async (
- userId: string,
- payload: { name: string; description?: string }
- ): Promise> => {
- const fullKey = createRandomApiKey();
- const keyHash = await hashApiKey(fullKey);
- const keyId = typeof crypto !== 'undefined' && 'randomUUID' in crypto
- ? crypto.randomUUID()
- : `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
-
- const record: StoredApiKeyRecord = {
- id: keyId,
- name: payload.name.trim(),
- keyPrefix: fullKey.slice(0, 8),
- keySuffix: fullKey.slice(-4),
- keyHash,
- createdAt: new Date().toISOString(),
- lastUsedAt: null,
- expiresAt: null,
- revokedAt: null,
- };
-
- if (payload.description?.trim()) {
- await userApi.setProfileAttribute(userId, `api_key_desc_${keyId}`, {
- value: payload.description.trim(),
- valueType: 'String',
- });
- }
-
- await userApi.setProfileAttribute(userId, toAttributeKey(keyId), {
- value: JSON.stringify(record),
- valueType: 'Json',
- });
-
- return {
- success: true,
- data: {
- apiKey: toPublicApiKey(record),
- key: fullKey,
- },
- timestamp: new Date().toISOString(),
- };
- },
-
- /**
- * EN: Revoke API key by marking profile attribute as revoked.
- * VI: Thu hồi API key bằng cách đánh dấu revoked trong profile attribute.
- */
- delete: async (userId: string, keyId: string): Promise> => {
- const profileResponse = await userApi.getProfile(userId);
- if (!profileResponse.success || !profileResponse.data) {
- throw new Error(profileResponse.error?.message || 'Failed to load API key');
- }
-
- const targetAttribute = profileResponse.data.attributes.find(
- (attribute) => attribute.key === toAttributeKey(keyId)
- );
-
- if (!targetAttribute) {
- return {
- success: true,
- timestamp: new Date().toISOString(),
- };
- }
-
- const record = parseStoredRecord(targetAttribute.value);
- if (!record) {
- throw new Error('Corrupted API key data');
- }
-
- const revokedRecord: StoredApiKeyRecord = {
- ...record,
- revokedAt: new Date().toISOString(),
- };
-
- await userApi.setProfileAttribute(userId, toAttributeKey(keyId), {
- value: JSON.stringify(revokedRecord),
- valueType: 'Json',
- });
-
- return {
- success: true,
- timestamp: new Date().toISOString(),
- };
- },
-};
diff --git a/apps/web-client/src/services/api/auth.api.ts b/apps/web-client/src/services/api/auth.api.ts
deleted file mode 100644
index 38cfb7b1..00000000
--- a/apps/web-client/src/services/api/auth.api.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { LoginDto, RegisterDto, AuthResponse, ApiResponse, UserResponse } from '@goodgo/types';
-
-import { apiClient } from './client';
-
-/**
- * EN: Authentication API service for frontend
- * VI: Service API xác thực cho frontend
- */
-export const authApi = {
- /**
- * EN: Register new user account
- * VI: Đăng ký tài khoản người dùng mới
- */
- register: async (data: RegisterDto): Promise> => {
- return apiClient.post('/auth/register', data);
- },
-
- /**
- * EN: Login user and store tokens
- * VI: Đăng nhập người dùng và lưu tokens
- */
- login: async (data: LoginDto): Promise> => {
- const response = await apiClient.post('/auth/login', data);
- // EN: Store tokens in client and localStorage on successful login
- // VI: Lưu tokens trong client và localStorage khi đăng nhập thành công
- if (response.success && response.data) {
- apiClient.setAuthToken(response.data.accessToken);
- if (typeof window !== 'undefined') {
- localStorage.setItem('refreshToken', response.data.refreshToken);
- }
- }
- return response;
- },
-
- /**
- * EN: Logout user and clear tokens
- * VI: Đăng xuất người dùng và xóa tokens
- */
- logout: async (): Promise => {
- // EN: Get refresh token from localStorage for logout request
- // VI: Lấy refresh token từ localStorage cho request logout
- const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
- const response = await apiClient.post('/auth/logout', { refreshToken });
- // EN: Clear tokens from client and localStorage
- // VI: Xóa tokens khỏi client và localStorage
- apiClient.removeAuthToken();
- if (typeof window !== 'undefined') {
- localStorage.removeItem('refreshToken');
- }
- return response;
- },
-
- /**
- * EN: Refresh access token using refresh token
- * VI: Làm mới access token sử dụng refresh token
- */
- refreshToken: async (refreshToken: string): Promise> => {
- const response = await apiClient.post('/auth/refresh', { refreshToken });
- // EN: Update access token in client on successful refresh
- // VI: Cập nhật access token trong client khi refresh thành công
- if (response.success && response.data) {
- apiClient.setAuthToken(response.data.accessToken);
- }
- return response;
- },
-
- /**
- * EN: Get current authenticated user profile
- * VI: Lấy hồ sơ người dùng đã xác thực hiện tại
- */
- getMe: async (): Promise> => {
- return apiClient.get('/identity/profile');
- },
-
- /**
- * EN: Change user password
- * VI: Thay đổi mật khẩu người dùng
- */
- changePassword: async (currentPassword: string, newPassword: string): Promise => {
- return apiClient.post('/auth/change-password', { currentPassword, newPassword });
- },
-
- /**
- * EN: Request password reset link via email
- * VI: Yêu cầu link đặt lại mật khẩu qua email
- */
- forgotPassword: async (email: string): Promise => {
- return apiClient.post('/auth/forgot-password', { email });
- },
-
- /**
- * EN: Reset password using reset token
- * VI: Đặt lại mật khẩu sử dụng reset token
- */
- resetPassword: async (token: string, newPassword: string): Promise => {
- return apiClient.post('/auth/reset-password', { token, newPassword });
- },
-
- /**
- * EN: Authenticate with OAuth token from callback
- * VI: Xác thực với OAuth token từ callback
- */
- oauthLogin: async (accessToken: string): Promise> => {
- // EN: Set the token in the client
- // VI: Đặt token trong client
- apiClient.setAuthToken(accessToken);
-
- // EN: Fetch user profile to complete authentication
- // VI: Lấy thông tin user để hoàn tất xác thực
- const userResponse = await apiClient.get('/identity/profile');
-
- if (userResponse.success && userResponse.data) {
- // EN: Store refresh token if available (OAuth might not provide refresh token)
- // VI: Lưu refresh token nếu có (OAuth có thể không cung cấp refresh token)
- // Note: For OAuth, we only have access token, refresh token handling depends on backend
- // Ghi chú: Đối với OAuth, chúng ta chỉ có access token, xử lý refresh token phụ thuộc vào backend
-
- return {
- success: true,
- data: {
- accessToken,
- refreshToken: '', // EN: OAuth may not provide refresh token / VI: OAuth có thể không cung cấp refresh token
- user: userResponse.data,
- },
- timestamp: new Date().toISOString(),
- };
- }
-
- throw new Error('Failed to fetch user profile / Không thể lấy thông tin người dùng');
- },
-
- /**
- * EN: Enable TOTP and get QR code
- * VI: Bật TOTP và lấy mã QR
- */
- enableTOTP: async (): Promise> => {
- return apiClient.post('/mfa/totp/enable', {});
- },
-
- /**
- * EN: Verify and enable TOTP with token
- * VI: Xác thực và bật TOTP với token
- */
- verifyAndEnableTOTP: async (secret: string, token: string): Promise => {
- return apiClient.post('/mfa/totp/verify', { secret, token });
- },
-
- /**
- * EN: Disable MFA
- * VI: Tắt MFA
- */
- disableMFA: async (): Promise => {
- return apiClient.post('/mfa/disable', {});
- },
-
- /**
- * EN: Get MFA devices
- * VI: Lấy thiết bị MFA
- */
- getMFADevices: async (): Promise>> => {
- return apiClient.get('/mfa/devices');
- },
-
- /**
- * EN: Get user sessions
- * VI: Lấy sessions của người dùng
- */
- getSessions: async (): Promise>> => {
- return apiClient.get('/sessions');
- },
-
- /**
- * EN: Revoke a session
- * VI: Thu hồi một session
- */
- revokeSession: async (sessionId: string): Promise => {
- return apiClient.delete(`/sessions/${sessionId}`);
- },
-
- /**
- * EN: Revoke all sessions
- * VI: Thu hồi tất cả sessions
- */
- revokeAllSessions: async (): Promise => {
- return apiClient.delete('/sessions');
- },
-};
diff --git a/apps/web-client/src/services/api/client.ts b/apps/web-client/src/services/api/client.ts
deleted file mode 100644
index 479d55d9..00000000
--- a/apps/web-client/src/services/api/client.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createHttpClient } from '@goodgo/http-client';
-
-// EN: Get API base URL from environment or use default
-// VI: Lấy API base URL từ environment hoặc sử dụng mặc định
-const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1';
-
-/**
- * EN: HTTP client instance configured for API calls
- * VI: Instance HTTP client đã cấu hình cho API calls
- */
-export const apiClient = createHttpClient({
- baseURL: API_URL,
- timeout: 30000, // EN: 30 second timeout / VI: Timeout 30 giây
-});
diff --git a/apps/web-client/src/services/api/storage.api.ts b/apps/web-client/src/services/api/storage.api.ts
deleted file mode 100644
index 40c2207c..00000000
--- a/apps/web-client/src/services/api/storage.api.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { ApiResponse } from '@goodgo/types';
-
-import { apiClient } from './client';
-
-/**
- * EN: Raw upload response from storage service.
- * VI: Response upload thô từ storage service.
- */
-interface UploadFileResult {
- success: boolean;
- fileId?: string;
- objectKey?: string;
- error?: string;
-}
-
-/**
- * EN: CDN URL response payload.
- * VI: Payload response URL CDN.
- */
-interface CdnUrlResult {
- url: string;
- isCDN: boolean;
- description: string;
-}
-
-/**
- * EN: Normalized avatar upload response.
- * VI: Response upload avatar đã chuẩn hóa.
- */
-export interface UploadAvatarResult {
- fileId: string;
- objectKey?: string;
- url: string;
-}
-
-/**
- * EN: Storage API for avatar upload flow.
- * VI: Storage API cho luồng upload avatar.
- */
-export const storageApi = {
- /**
- * EN: Upload avatar as a public file and resolve CDN/fallback URL.
- * VI: Upload avatar ở chế độ public và lấy URL CDN/fallback.
- */
- uploadAvatar: async (file: File): Promise> => {
- const formData = new FormData();
- formData.append('file', file);
-
- const uploadResponse = await apiClient.post(
- '/files/upload?accessLevel=public',
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- }
- );
-
- const uploadedFileId = uploadResponse.data?.fileId;
- if (!uploadResponse.success || !uploadResponse.data?.success || !uploadedFileId) {
- throw new Error(
- uploadResponse.data?.error ||
- uploadResponse.error?.message ||
- 'Failed to upload avatar'
- );
- }
-
- let resolvedUrl = '';
- try {
- const cdnUrlResponse = await apiClient.get(
- `/files/${uploadedFileId}/cdn-url`
- );
- resolvedUrl = cdnUrlResponse.data?.url ?? '';
- } catch {
- // EN: Fallback keeps profile update possible even if CDN URL fetch fails.
- // VI: Fallback vẫn cho phép cập nhật profile nếu lấy CDN URL thất bại.
- resolvedUrl = '';
- }
-
- return {
- success: true,
- data: {
- fileId: uploadedFileId,
- objectKey: uploadResponse.data.objectKey,
- url: resolvedUrl,
- },
- timestamp: new Date().toISOString(),
- };
- },
-};
diff --git a/apps/web-client/src/services/api/user.api.ts b/apps/web-client/src/services/api/user.api.ts
deleted file mode 100644
index 3543562d..00000000
--- a/apps/web-client/src/services/api/user.api.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { ApiResponse } from '@goodgo/types';
-
-import { apiClient } from './client';
-
-/**
- * EN: User profile interface
- * VI: Interface profile người dùng
- */
-export interface UserProfile {
- id: string;
- userId: string;
- bio?: string;
- avatarUrl?: string;
- timezone?: string;
- locale?: string;
- phoneNumber?: {
- countryCode: string;
- nationalNumber: string;
- };
- attributes: Array<{
- key: string;
- value: string;
- valueType: string;
- }>;
- createdAt: string;
- updatedAt?: string;
-}
-
-/**
- * EN: Update user profile DTO
- * VI: DTO cập nhật profile người dùng
- */
-export interface UpdateUserProfileDto {
- bio?: string;
- timezone?: string;
- locale?: string;
- avatarUrl?: string | null;
-}
-
-/**
- * EN: Set profile attribute DTO.
- * VI: DTO đặt thuộc tính profile.
- */
-export interface SetProfileAttributeDto {
- value: string;
- valueType?: 'String' | 'Number' | 'Boolean' | 'Date' | 'Json';
-}
-
-/**
- * EN: User API service for profile management
- * VI: Service API người dùng để quản lý profile
- */
-export const userApi = {
- /**
- * EN: Get user profile by user ID
- * VI: Lấy profile người dùng theo user ID
- *
- * @param userId - User ID / ID người dùng
- */
- getProfile: async (userId: string): Promise> => {
- return apiClient.get(`/users/${userId}/profile`);
- },
-
- /**
- * EN: Update user profile
- * VI: Cập nhật profile người dùng
- *
- * @param userId - User ID / ID người dùng
- * @param data - Profile data to update / Dữ liệu profile cần cập nhật
- */
- updateProfile: async (
- userId: string,
- data: UpdateUserProfileDto
- ): Promise> => {
- return apiClient.put(`/users/${userId}/profile`, data);
- },
-
- /**
- * EN: Set custom profile attribute.
- * VI: Đặt thuộc tính tùy chỉnh cho profile.
- */
- setProfileAttribute: async (
- userId: string,
- key: string,
- payload: SetProfileAttributeDto
- ): Promise> => {
- return apiClient.put(
- `/users/${userId}/profile/attributes/${encodeURIComponent(key)}`,
- {
- value: payload.value,
- valueType: payload.valueType ?? 'String',
- }
- );
- },
-
- /**
- * EN: Upload avatar image
- * VI: Upload ảnh avatar
- *
- * @param userId - User ID / ID người dùng
- * @param avatarUrl - Avatar URL / URL avatar
- */
- uploadAvatar: async (
- userId: string,
- avatarUrl: string
- ): Promise> => {
- return apiClient.put(`/users/${userId}/profile`, { avatarUrl });
- },
-
- /**
- * EN: Delete avatar
- * VI: Xóa avatar
- *
- * @param userId - User ID / ID người dùng
- */
- deleteAvatar: async (userId: string): Promise => {
- return apiClient.put(`/users/${userId}/profile`, { avatarUrl: null });
- },
-};
diff --git a/apps/web-client/src/stores/__tests__/auth-store.test.ts b/apps/web-client/src/stores/__tests__/auth-store.test.ts
deleted file mode 100644
index ed2ca60b..00000000
--- a/apps/web-client/src/stores/__tests__/auth-store.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { useAuthStore } from '../auth-store';
-
-/**
- * EN: Auth store unit tests
- * VI: Unit tests cho auth store
- */
-describe('AuthStore', () => {
- beforeEach(() => {
- // EN: Reset store state before each test / VI: Reset state store trước mỗi test
- useAuthStore.setState({
- user: null,
- isAuthenticated: false,
- isLoading: false,
- error: null,
- });
- });
-
- it('initializes with default state', () => {
- const state = useAuthStore.getState();
- expect(state.user).toBeNull();
- expect(state.isAuthenticated).toBe(false);
- expect(state.isLoading).toBe(false);
- });
-
- it('sets user on login', async () => {
- const mockUser = {
- id: '1',
- email: 'test@example.com',
- role: 'user',
- };
-
- // EN: Mock login function / VI: Mock hàm login
- const login = useAuthStore.getState().login;
- // EN: Note: This is a simplified test - actual implementation would require API mocking
- // VI: Lưu ý: Đây là test đơn giản - implementation thực tế sẽ cần mock API
- expect(login).toBeDefined();
- });
-
- it('clears user on logout', () => {
- const logout = useAuthStore.getState().logout;
- expect(logout).toBeDefined();
- });
-});
diff --git a/apps/web-client/src/stores/__tests__/chat-store.test.ts b/apps/web-client/src/stores/__tests__/chat-store.test.ts
deleted file mode 100644
index 212d2463..00000000
--- a/apps/web-client/src/stores/__tests__/chat-store.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { useChatStore, MessageSender, MessageStatus } from '../chat-store';
-
-/**
- * EN: Chat store unit tests
- * VI: Unit tests cho chat store
- */
-describe('ChatStore', () => {
- beforeEach(() => {
- // EN: Reset store state before each test / VI: Reset state store trước mỗi test
- useChatStore.setState({
- conversations: [],
- messages: {},
- currentConversationId: null,
- isLoading: false,
- error: null,
- });
- });
-
- it('initializes with default state', () => {
- const state = useChatStore.getState();
- expect(state.conversations).toEqual([]);
- expect(state.messages).toEqual({});
- expect(state.currentConversationId).toBeNull();
- });
-
- it('creates new conversation', () => {
- const createConversation = useChatStore.getState().createConversation;
- const conversationId = createConversation('Test Conversation');
-
- expect(conversationId).toBeDefined();
- const state = useChatStore.getState();
- expect(state.conversations.length).toBe(1);
- expect(state.conversations[0].title).toBe('Test Conversation');
- });
-
- it('selects conversation', () => {
- const createConversation = useChatStore.getState().createConversation;
- const selectConversation = useChatStore.getState().selectConversation;
-
- const conversationId = createConversation('Test');
- selectConversation(conversationId);
-
- const state = useChatStore.getState();
- expect(state.currentConversationId).toBe(conversationId);
- });
-});
diff --git a/apps/web-client/src/stores/__tests__/users-store.test.ts b/apps/web-client/src/stores/__tests__/users-store.test.ts
deleted file mode 100644
index a704f57a..00000000
--- a/apps/web-client/src/stores/__tests__/users-store.test.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { useUsersStore } from '../users-store';
-
-// Mock the API functions
-vi.mock('../../lib/api/users', () => ({
- getUsers: vi.fn(),
- getUser: vi.fn(),
- createUser: vi.fn(),
- updateUser: vi.fn(),
- deleteUser: vi.fn(),
- bulkDeleteUsers: vi.fn(),
- bulkUpdateUserRoles: vi.fn(),
-}));
-
-import {
- getUsers,
- getUser,
- createUser,
- updateUser,
- deleteUser,
- bulkDeleteUsers,
- bulkUpdateUserRoles,
-} from '../../lib/api/users';
-
-/**
- * EN: Users store unit tests
- * VI: Unit tests cho users store
- */
-describe('UsersStore', () => {
- beforeEach(() => {
- // Reset store state before each test
- useUsersStore.setState({
- users: [],
- currentUser: null,
- pagination: null,
- isLoading: false,
- isLoadingUser: false,
- error: null,
- });
-
- // Reset all mocks
- vi.clearAllMocks();
- });
-
- describe('initial state', () => {
- it('initializes with default state', () => {
- const state = useUsersStore.getState();
- expect(state.users).toEqual([]);
- expect(state.currentUser).toBeNull();
- expect(state.pagination).toBeNull();
- expect(state.isLoading).toBe(false);
- expect(state.isLoadingUser).toBe(false);
- expect(state.error).toBeNull();
- });
- });
-
- describe('fetchUsers', () => {
- it('sets loading state and fetches users successfully', async () => {
- const mockResponse = {
- data: [
- {
- id: '1',
- email: 'user1@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-01T00:00:00Z',
- updatedAt: '2024-01-01T00:00:00Z',
- },
- ],
- pagination: {
- page: 1,
- limit: 10,
- total: 1,
- totalPages: 1,
- },
- };
-
- (getUsers as any).mockResolvedValue(mockResponse);
-
- const { fetchUsers } = useUsersStore.getState();
- await fetchUsers();
-
- const state = useUsersStore.getState();
- expect(state.isLoading).toBe(false);
- expect(state.users).toEqual(mockResponse.data);
- expect(state.pagination).toEqual(mockResponse.pagination);
- expect(state.error).toBeNull();
- });
-
- it('handles fetch users error', async () => {
- const mockError = new Error('Failed to fetch users');
- (getUsers as any).mockRejectedValue(mockError);
-
- const { fetchUsers } = useUsersStore.getState();
- await expect(fetchUsers()).rejects.toThrow('Failed to fetch users');
-
- const state = useUsersStore.getState();
- expect(state.isLoading).toBe(false);
- expect(state.users).toEqual([]);
- expect(state.error).toBe('Failed to fetch users');
- });
- });
-
- describe('fetchUser', () => {
- it('fetches single user successfully', async () => {
- const mockUser = {
- id: '1',
- email: 'user1@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-01T00:00:00Z',
- updatedAt: '2024-01-01T00:00:00Z',
- };
-
- (getUser as any).mockResolvedValue(mockUser);
-
- const { fetchUser } = useUsersStore.getState();
- await fetchUser('1');
-
- const state = useUsersStore.getState();
- expect(state.isLoadingUser).toBe(false);
- expect(state.currentUser).toEqual(mockUser);
- expect(state.error).toBeNull();
- });
-
- it('handles fetch user error', async () => {
- const mockError = new Error('User not found');
- (getUser as any).mockRejectedValue(mockError);
-
- const { fetchUser } = useUsersStore.getState();
- await expect(fetchUser('1')).rejects.toThrow('User not found');
-
- const state = useUsersStore.getState();
- expect(state.isLoadingUser).toBe(false);
- expect(state.currentUser).toBeNull();
- expect(state.error).toBe('User not found');
- });
- });
-
- describe('createUser', () => {
- it('creates user successfully and adds to list', async () => {
- const newUser = {
- id: '3',
- email: 'user3@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-03T00:00:00Z',
- updatedAt: '2024-01-03T00:00:00Z',
- };
-
- const existingUsers = [
- {
- id: '1',
- email: 'user1@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-01T00:00:00Z',
- updatedAt: '2024-01-01T00:00:00Z',
- },
- {
- id: '2',
- email: 'user2@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-02T00:00:00Z',
- updatedAt: '2024-01-02T00:00:00Z',
- },
- ];
-
- // Set initial state with existing users
- useUsersStore.setState({ users: existingUsers });
-
- (createUser as any).mockResolvedValue(newUser);
-
- const createData = {
- email: 'user3@example.com',
- password: 'password123',
- role: 'USER' as const,
- };
-
- const { createUser: createUserAction } = useUsersStore.getState();
- const result = await createUserAction(createData);
-
- expect(result).toEqual(newUser);
-
- const state = useUsersStore.getState();
- expect(state.isLoadingUser).toBe(false);
- expect(state.users).toHaveLength(2);
- expect(state.users[0].id).toBe('1');
- });
- });
-
- describe('updateUser', () => {
- it('updates user successfully', async () => {
- const existingUser = {
- id: '1',
- email: 'user1@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-01T00:00:00Z',
- updatedAt: '2024-01-01T00:00:00Z',
- };
-
- const updatedUser = {
- ...existingUser,
- email: 'updated@example.com',
- role: 'ADMIN',
- };
-
- useUsersStore.setState({
- users: [existingUser],
- currentUser: existingUser,
- });
-
- (updateUser as any).mockResolvedValue(updatedUser);
-
- const updateData = {
- email: 'updated@example.com',
- role: 'ADMIN' as const,
- isActive: true,
- };
-
- const { updateUser: updateUserAction } = useUsersStore.getState();
- const result = await updateUserAction('1', updateData);
-
- expect(result).toEqual(updatedUser);
-
- const state = useUsersStore.getState();
- expect(state.isLoadingUser).toBe(false);
- expect(state.users[0]).toEqual(updatedUser);
- expect(state.currentUser).toEqual(updatedUser);
- });
- });
-
- describe('deleteUser', () => {
- it('deletes user successfully', async () => {
- const userToDelete = {
- id: '2',
- email: 'user2@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-02T00:00:00Z',
- updatedAt: '2024-01-02T00:00:00Z',
- };
-
- const users = [
- {
- id: '1',
- email: 'user1@example.com',
- role: 'USER',
- isActive: true,
- createdAt: '2024-01-01T00:00:00Z',
- updatedAt: '2024-01-01T00:00:00Z',
- },
- userToDelete,
- ];
-
- useUsersStore.setState({ users, currentUser: userToDelete });
-
- (deleteUser as any).mockResolvedValue(undefined);
-
- const { deleteUser: deleteUserAction } = useUsersStore.getState();
- await deleteUserAction('2');
-
- const state = useUsersStore.getState();
- expect(state.isLoadingUser).toBe(false);
- expect(state.users).toHaveLength(1);
- expect(state.users[0].id).toBe('1');
- expect(state.currentUser).toBeNull();
- });
- });
-
- describe('error handling', () => {
- it('clears error state', () => {
- useUsersStore.setState({ error: 'Test error' });
- const { clearError } = useUsersStore.getState();
-
- clearError();
-
- const state = useUsersStore.getState();
- expect(state.error).toBeNull();
- });
-
- it('resets store to initial state', () => {
- useUsersStore.setState({
- users: [{ id: '1', email: 'test@example.com', role: 'USER', isActive: true, createdAt: '', updatedAt: '' }],
- currentUser: { id: '1', email: 'test@example.com', role: 'USER', isActive: true, createdAt: '', updatedAt: '' },
- pagination: { page: 1, limit: 10, total: 1, totalPages: 1 },
- isLoading: true,
- isLoadingUser: true,
- error: 'Test error',
- });
-
- const { reset } = useUsersStore.getState();
- reset();
-
- const state = useUsersStore.getState();
- expect(state.users).toEqual([]);
- expect(state.currentUser).toBeNull();
- expect(state.pagination).toBeNull();
- expect(state.isLoading).toBe(false);
- expect(state.isLoadingUser).toBe(false);
- expect(state.error).toBeNull();
- });
- });
-});
\ No newline at end of file
diff --git a/apps/web-client/src/stores/auth-store.ts b/apps/web-client/src/stores/auth-store.ts
deleted file mode 100644
index 7a2001bf..00000000
--- a/apps/web-client/src/stores/auth-store.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import { UserResponse } from '@goodgo/types';
-import { create } from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { authApi } from '../services/api/auth.api';
-
-/**
- * EN: Authentication state interface for Zustand store
- * VI: Interface trạng thái xác thực cho Zustand store
- */
-interface AuthState {
- /** EN: Current authenticated user / VI: Người dùng đã xác thực hiện tại */
- user: UserResponse | null;
- /** EN: Authentication status / VI: Trạng thái xác thực */
- isAuthenticated: boolean;
- /** EN: Loading state for async operations / VI: Trạng thái loading cho async operations */
- isLoading: boolean;
- /** EN: Login method / VI: Method đăng nhập */
- login: (email: string, password: string) => Promise;
- /** EN: Register method / VI: Method đăng ký */
- register: (email: string, password: string, confirmPassword: string) => Promise;
- /** EN: Logout method / VI: Method đăng xuất */
- logout: () => Promise;
- /** EN: Fetch current user method / VI: Method lấy thông tin user hiện tại */
- fetchUser: () => Promise;
- /** EN: OAuth login method / VI: Method đăng nhập OAuth */
- oauthLogin: (accessToken: string) => Promise;
- /** EN: Update profile method / VI: Method cập nhật profile */
- updateProfile: (data: Partial) => void;
-}
-
-/**
- * EN: Zustand store for authentication state management with persistence
- * VI: Zustand store để quản lý trạng thái xác thực với persistence
- *
- * Features:
- * - User state management (authenticated user data)
- * - Authentication status tracking
- * - Loading states for async operations
- * - Login, register, logout operations
- * - OAuth authentication support
- * - Persistent storage with localStorage (user and auth status only)
- */
-export const useAuthStore = create()(
- persist(
- (set) => ({
- user: null,
- isAuthenticated: false,
- isLoading: false,
-
- /**
- * EN: Login user and update store state
- * VI: Đăng nhập người dùng và cập nhật trạng thái store
- *
- * @param email - User email address / Địa chỉ email người dùng
- * @param password - User password / Mật khẩu người dùng
- * @throws Error if login fails / Ném lỗi nếu đăng nhập thất bại
- */
- login: async (email: string, password: string) => {
- set({ isLoading: true });
- try {
- const response = await authApi.login({ email, password });
- if (response.success && response.data) {
- set({
- user: response.data.user,
- isAuthenticated: true,
- isLoading: false,
- });
- } else {
- throw new Error(response.error?.message || 'Login failed');
- }
- } catch (error) {
- set({ isLoading: false });
- throw error;
- }
- },
-
- /**
- * EN: Register new user and update store state
- * VI: Đăng ký người dùng mới và cập nhật trạng thái store
- *
- * @param email - User email address / Địa chỉ email người dùng
- * @param password - User password / Mật khẩu người dùng
- * @param confirmPassword - Password confirmation / Xác nhận mật khẩu
- * @throws Error if registration fails / Ném lỗi nếu đăng ký thất bại
- */
- register: async (email: string, password: string, confirmPassword: string) => {
- set({ isLoading: true });
- try {
- const response = await authApi.register({ email, password, confirmPassword });
- if (response.success && response.data) {
- set({
- user: response.data.user,
- isAuthenticated: true,
- isLoading: false,
- });
- } else {
- throw new Error(response.error?.message || 'Registration failed');
- }
- } catch (error) {
- set({ isLoading: false });
- throw error;
- }
- },
-
- /**
- * EN: Logout user and clear store state
- * VI: Đăng xuất người dùng và xóa trạng thái store
- *
- * Clears tokens and user data from both store and localStorage
- * Xóa tokens và dữ liệu người dùng khỏi cả store và localStorage
- */
- logout: async () => {
- try {
- await authApi.logout();
- } finally {
- // EN: Always clear state even if logout API call fails
- // VI: Luôn xóa trạng thái ngay cả khi API call logout thất bại
- set({
- user: null,
- isAuthenticated: false,
- });
- }
- },
-
- /**
- * EN: Fetch current user profile from API
- * VI: Lấy hồ sơ người dùng hiện tại từ API
- *
- * Used for checking authentication status on app initialization
- * Được sử dụng để kiểm tra trạng thái xác thực khi khởi tạo ứng dụng
- */
- fetchUser: async () => {
- set({ isLoading: true });
- try {
- const response = await authApi.getMe();
- if (response.success && response.data) {
- set({
- user: response.data,
- isAuthenticated: true,
- isLoading: false,
- });
- } else {
- // EN: Clear state if user fetch fails (token might be invalid)
- // VI: Xóa trạng thái nếu fetch user thất bại (token có thể không hợp lệ)
- set({
- user: null,
- isAuthenticated: false,
- isLoading: false,
- });
- }
- } catch (error) {
- // EN: Clear user state on fetch failure (token expired or invalid)
- // VI: Xóa trạng thái user khi fetch thất bại (token hết hạn hoặc không hợp lệ)
- set({
- user: null,
- isAuthenticated: false,
- isLoading: false,
- });
- }
- },
-
- /**
- * EN: Login with OAuth token from callback
- * VI: Đăng nhập với OAuth token từ callback
- *
- * @param accessToken - OAuth access token / OAuth access token
- * @throws Error if OAuth login fails / Ném lỗi nếu đăng nhập OAuth thất bại
- */
- oauthLogin: async (accessToken: string) => {
- set({ isLoading: true });
- try {
- const response = await authApi.oauthLogin(accessToken);
- if (response.success && response.data) {
- // EN: Store refresh token if provided
- // VI: Lưu refresh token nếu được cung cấp
- if (response.data.refreshToken && typeof window !== 'undefined') {
- localStorage.setItem('refreshToken', response.data.refreshToken);
- }
- set({
- user: response.data.user,
- isAuthenticated: true,
- isLoading: false,
- });
- } else {
- throw new Error(response.error?.message || 'OAuth login failed / Đăng nhập OAuth thất bại');
- }
- } catch (error) {
- set({ isLoading: false });
- throw error;
- }
- },
-
- /**
- * EN: Update user profile in store
- * VI: Cập nhật hồ sơ người dùng trong store
- *
- * @param data - Updated user data / Dữ liệu người dùng đã cập nhật
- */
- updateProfile: (data: Partial) => {
- set((state) => ({
- user: state.user ? { ...state.user, ...data } : null,
- }));
- },
- }),
- {
- // EN: Persist auth state to localStorage
- // VI: Persist trạng thái auth vào localStorage
- name: 'auth-storage',
- // EN: Only persist user and isAuthenticated, exclude isLoading
- // VI: Chỉ persist user và isAuthenticated, loại trừ isLoading
- partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }),
- }
- )
-);
diff --git a/apps/web-client/src/stores/chat-store.ts b/apps/web-client/src/stores/chat-store.ts
deleted file mode 100644
index fbe578f9..00000000
--- a/apps/web-client/src/stores/chat-store.ts
+++ /dev/null
@@ -1,560 +0,0 @@
-import { create } from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { WebSocketClient, WebSocketState, WebSocketMessage, WebSocketMessageType } from '../features/chat/lib/websocket';
-
-/**
- * EN: Message sender type
- * VI: Loại người gửi tin nhắn
- */
-export enum MessageSender {
- /** EN: User message / VI: Tin nhắn từ người dùng */
- USER = 'user',
- /** EN: AI/Assistant message / VI: Tin nhắn từ AI/Assistant */
- ASSISTANT = 'assistant',
- /** EN: System message / VI: Tin nhắn hệ thống */
- SYSTEM = 'system',
-}
-
-/**
- * EN: Message status
- * VI: Trạng thái tin nhắn
- */
-export enum MessageStatus {
- /** EN: Message is sending / VI: Tin nhắn đang được gửi */
- SENDING = 'sending',
- /** EN: Message is sent / VI: Tin nhắn đã được gửi */
- SENT = 'sent',
- /** EN: Message failed to send / VI: Gửi tin nhắn thất bại */
- FAILED = 'failed',
- /** EN: Message is delivered / VI: Tin nhắn đã được gửi đến */
- DELIVERED = 'delivered',
- /** EN: Message is read / VI: Tin nhắn đã được đọc */
- READ = 'read',
-}
-
-/**
- * EN: Chat message interface
- * VI: Interface tin nhắn chat
- */
-export interface ChatMessage {
- /** EN: Unique message identifier / VI: Mã định danh tin nhắn duy nhất */
- id: string;
- /** EN: Conversation ID this message belongs to / VI: ID cuộc trò chuyện mà tin nhắn này thuộc về */
- conversationId: string;
- /** EN: Message content / VI: Nội dung tin nhắn */
- content: string;
- /** EN: Message sender / VI: Người gửi tin nhắn */
- sender: MessageSender;
- /** EN: Message status / VI: Trạng thái tin nhắn */
- status: MessageStatus;
- /** EN: Message creation timestamp / VI: Timestamp tạo tin nhắn */
- createdAt: string;
- /** EN: Message update timestamp / VI: Timestamp cập nhật tin nhắn */
- updatedAt?: string;
- /** EN: Optional metadata / VI: Metadata tùy chọn */
- metadata?: Record;
-}
-
-/**
- * EN: Conversation interface
- * VI: Interface cuộc trò chuyện
- */
-export interface Conversation {
- /** EN: Unique conversation identifier / VI: Mã định danh cuộc trò chuyện duy nhất */
- id: string;
- /** EN: Conversation title / VI: Tiêu đề cuộc trò chuyện */
- title: string;
- /** EN: Last message in conversation / VI: Tin nhắn cuối trong cuộc trò chuyện */
- lastMessage?: ChatMessage;
- /** EN: Last activity timestamp / VI: Timestamp hoạt động cuối */
- updatedAt: string;
- /** EN: Conversation creation timestamp / VI: Timestamp tạo cuộc trò chuyện */
- createdAt: string;
- /** EN: Number of unread messages / VI: Số tin nhắn chưa đọc */
- unreadCount?: number;
-}
-
-/**
- * EN: Chat state interface for Zustand store
- * VI: Interface trạng thái chat cho Zustand store
- */
-interface ChatState {
- /** EN: WebSocket client instance / VI: Instance WebSocket client */
- wsClient: WebSocketClient | null;
- /** EN: WebSocket connection state / VI: Trạng thái kết nối WebSocket */
- connectionState: WebSocketState;
- /** EN: Current conversation ID / VI: ID cuộc trò chuyện hiện tại */
- currentConversationId: string | null;
- /** EN: List of conversations / VI: Danh sách cuộc trò chuyện */
- conversations: Conversation[];
- /** EN: Messages map by conversation ID / VI: Map tin nhắn theo ID cuộc trò chuyện */
- messages: Record;
- /** EN: Loading state / VI: Trạng thái loading */
- isLoading: boolean;
- /** EN: Error message / VI: Thông báo lỗi */
- error: string | null;
- /** EN: Typing indicator state (userId -> boolean) / VI: Trạng thái chỉ báo đang gõ (userId -> boolean) */
- typingUsers: Record;
- /** EN: Initialize WebSocket connection / VI: Khởi tạo kết nối WebSocket */
- initializeWebSocket: (url: string) => void;
- /** EN: Connect WebSocket / VI: Kết nối WebSocket */
- connect: () => void;
- /** EN: Disconnect WebSocket / VI: Ngắt kết nối WebSocket */
- disconnect: () => void;
- /** EN: Send message / VI: Gửi tin nhắn */
- sendMessage: (conversationId: string, content: string) => Promise;
- /** EN: Receive message / VI: Nhận tin nhắn */
- receiveMessage: (message: ChatMessage) => void;
- /** EN: Create new conversation / VI: Tạo cuộc trò chuyện mới */
- createConversation: (title?: string) => string;
- /** EN: Select conversation / VI: Chọn cuộc trò chuyện */
- selectConversation: (conversationId: string) => void;
- /** EN: Delete conversation / VI: Xóa cuộc trò chuyện */
- deleteConversation: (conversationId: string) => void;
- /** EN: Update message status / VI: Cập nhật trạng thái tin nhắn */
- updateMessageStatus: (messageId: string, conversationId: string, status: MessageStatus) => void;
- /** EN: Set typing indicator / VI: Đặt chỉ báo đang gõ */
- setTyping: (userId: string, isTyping: boolean) => void;
- /** EN: Clear error / VI: Xóa lỗi */
- clearError: () => void;
-}
-
-/**
- * EN: Generate unique ID for messages/conversations
- * VI: Tạo ID duy nhất cho messages/conversations
- */
-const generateId = (): string => {
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
-};
-
-/**
- * EN: Zustand store for chat state management with WebSocket integration
- * VI: Zustand store để quản lý trạng thái chat với tích hợp WebSocket
- *
- * Features:
- * - WebSocket connection management
- * - Message state management
- * - Conversation state management
- * - Real-time message sending/receiving
- * - Typing indicators
- * - Persistent storage (conversations and messages)
- */
-export const useChatStore = create()(
- persist(
- (set, get) => ({
- wsClient: null,
- connectionState: WebSocketState.CLOSED,
- currentConversationId: null,
- conversations: [],
- messages: {},
- isLoading: false,
- error: null,
- typingUsers: {},
-
- /**
- * EN: Initialize WebSocket client with URL
- * VI: Khởi tạo WebSocket client với URL
- *
- * @param url - WebSocket server URL / URL server WebSocket
- */
- initializeWebSocket: (url: string) => {
- const { wsClient } = get();
-
- // EN: Clean up existing connection if any
- // VI: Dọn dẹp kết nối hiện có nếu có
- if (wsClient) {
- wsClient.disconnect();
- }
-
- // EN: Create new WebSocket client
- // VI: Tạo WebSocket client mới
- const client = new WebSocketClient({
- url,
- autoReconnect: true,
- maxReconnectAttempts: 10,
- reconnectDelay: 1000,
- callbacks: {
- onOpen: () => {
- set({ connectionState: WebSocketState.CONNECTED });
- },
- onClose: () => {
- set({ connectionState: WebSocketState.CLOSED });
- },
- onError: (error) => {
- console.error('WebSocket error / Lỗi WebSocket:', error);
- set({
- connectionState: WebSocketState.ERROR,
- error: 'Connection error / Lỗi kết nối',
- });
- },
- onStateChange: (state) => {
- set({ connectionState: state });
- },
- onMessage: (wsMessage: WebSocketMessage) => {
- const state = get();
-
- // EN: Handle different message types
- // VI: Xử lý các loại tin nhắn khác nhau
- switch (wsMessage.type) {
- case WebSocketMessageType.MESSAGE:
- if (wsMessage.data) {
- const chatMessage: ChatMessage = {
- id: wsMessage.messageId || generateId(),
- conversationId: wsMessage.conversationId || state.currentConversationId || '',
- content: (wsMessage.data && typeof wsMessage.data === 'object' && 'content' in wsMessage.data && typeof wsMessage.data.content === 'string') ? wsMessage.data.content : '',
- sender: (wsMessage.data && typeof wsMessage.data === 'object' && 'sender' in wsMessage.data) ? (wsMessage.data.sender as MessageSender) : MessageSender.ASSISTANT,
- status: MessageStatus.DELIVERED,
- createdAt: wsMessage.timestamp || new Date().toISOString(),
- metadata: (wsMessage.data && typeof wsMessage.data === 'object' && 'metadata' in wsMessage.data) ? (wsMessage.data.metadata as Record) : undefined,
- };
- state.receiveMessage(chatMessage);
- }
- break;
- case WebSocketMessageType.TYPING:
- if (wsMessage.data && typeof wsMessage.data === 'object' && 'userId' in wsMessage.data && typeof wsMessage.data.userId === 'string') {
- const isTyping = (wsMessage.data && typeof wsMessage.data === 'object' && 'isTyping' in wsMessage.data) ? Boolean(wsMessage.data.isTyping) : false;
- state.setTyping(wsMessage.data.userId, isTyping);
- }
- break;
- case WebSocketMessageType.READ:
- if (wsMessage.messageId && wsMessage.conversationId) {
- state.updateMessageStatus(
- wsMessage.messageId,
- wsMessage.conversationId,
- MessageStatus.READ
- );
- }
- break;
- case WebSocketMessageType.ERROR: {
- const errorMessage = (wsMessage.data && typeof wsMessage.data === 'object' && 'message' in wsMessage.data && typeof wsMessage.data.message === 'string')
- ? wsMessage.data.message
- : 'Unknown error / Lỗi không xác định';
- set({ error: errorMessage });
- break;
- }
- default:
- console.log('Unhandled WebSocket message type / Loại tin nhắn WebSocket chưa xử lý:', wsMessage.type);
- }
- },
- },
- });
-
- set({ wsClient: client });
- },
-
- /**
- * EN: Connect to WebSocket server
- * VI: Kết nối tới server WebSocket
- */
- connect: () => {
- const { wsClient } = get();
- if (!wsClient) {
- throw new Error('WebSocket client not initialized. Call initializeWebSocket first. / WebSocket client chưa được khởi tạo. Gọi initializeWebSocket trước.');
- }
- wsClient.connect();
- },
-
- /**
- * EN: Disconnect from WebSocket server
- * VI: Ngắt kết nối khỏi server WebSocket
- */
- disconnect: () => {
- const { wsClient } = get();
- if (wsClient) {
- wsClient.disconnect();
- }
- set({ connectionState: WebSocketState.CLOSED });
- },
-
- /**
- * EN: Send message through WebSocket
- * VI: Gửi tin nhắn qua WebSocket
- *
- * @param conversationId - Conversation ID / ID cuộc trò chuyện
- * @param content - Message content / Nội dung tin nhắn
- */
- sendMessage: async (conversationId: string, content: string) => {
- const { wsClient, currentConversationId } = get();
-
- // EN: Ensure conversation is selected
- // VI: Đảm bảo cuộc trò chuyện được chọn
- if (currentConversationId !== conversationId) {
- get().selectConversation(conversationId);
- }
-
- if (!wsClient || !wsClient.isConnected()) {
- throw new Error('WebSocket is not connected / WebSocket chưa kết nối');
- }
-
- // EN: Create temporary message with SENDING status
- // VI: Tạo tin nhắn tạm thời với trạng thái SENDING
- const tempMessageId = generateId();
- const tempMessage: ChatMessage = {
- id: tempMessageId,
- conversationId,
- content,
- sender: MessageSender.USER,
- status: MessageStatus.SENDING,
- createdAt: new Date().toISOString(),
- };
-
- // EN: Add temporary message to state
- // VI: Thêm tin nhắn tạm thời vào state
- set((state) => {
- const messages = state.messages[conversationId] || [];
- return {
- messages: {
- ...state.messages,
- [conversationId]: [...messages, tempMessage],
- },
- };
- });
-
- try {
- // EN: Send message via WebSocket
- // VI: Gửi tin nhắn qua WebSocket
- wsClient.send({
- type: WebSocketMessageType.MESSAGE,
- conversationId,
- messageId: tempMessageId,
- data: {
- content,
- sender: MessageSender.USER,
- },
- timestamp: new Date().toISOString(),
- });
-
- // EN: Update message status to SENT
- // VI: Cập nhật trạng thái tin nhắn thành SENT
- get().updateMessageStatus(tempMessageId, conversationId, MessageStatus.SENT);
- } catch (error) {
- console.error('Failed to send message / Không thể gửi tin nhắn:', error);
- // EN: Update message status to FAILED
- // VI: Cập nhật trạng thái tin nhắn thành FAILED
- get().updateMessageStatus(tempMessageId, conversationId, MessageStatus.FAILED);
- throw error;
- }
- },
-
- /**
- * EN: Receive message and add to state
- * VI: Nhận tin nhắn và thêm vào state
- *
- * @param message - Received message / Tin nhắn đã nhận
- */
- receiveMessage: (message: ChatMessage) => {
- set((state) => {
- const messages = state.messages[message.conversationId] || [];
-
- // EN: Check if message already exists (avoid duplicates)
- // VI: Kiểm tra nếu tin nhắn đã tồn tại (tránh trùng lặp)
- const existingIndex = messages.findIndex((m) => m.id === message.id);
- if (existingIndex >= 0) {
- // EN: Update existing message
- // VI: Cập nhật tin nhắn hiện có
- const updatedMessages = [...messages];
- updatedMessages[existingIndex] = message;
- return {
- messages: {
- ...state.messages,
- [message.conversationId]: updatedMessages,
- },
- };
- }
-
- // EN: Add new message
- // VI: Thêm tin nhắn mới
- return {
- messages: {
- ...state.messages,
- [message.conversationId]: [...messages, message],
- },
- };
- });
-
- // EN: Update conversation's last message
- // VI: Cập nhật tin nhắn cuối của cuộc trò chuyện
- set((state) => {
- const conversations = state.conversations.map((conv) => {
- if (conv.id === message.conversationId) {
- return {
- ...conv,
- lastMessage: message,
- updatedAt: message.createdAt,
- unreadCount: conv.id === state.currentConversationId
- ? 0
- : (conv.unreadCount || 0) + 1,
- };
- }
- return conv;
- });
-
- // EN: Create conversation if it doesn't exist
- // VI: Tạo cuộc trò chuyện nếu chưa tồn tại
- if (!conversations.find((c) => c.id === message.conversationId)) {
- const newConversation: Conversation = {
- id: message.conversationId,
- title: message.content.substring(0, 50) || 'New Conversation / Cuộc trò chuyện mới',
- lastMessage: message,
- updatedAt: message.createdAt,
- createdAt: message.createdAt,
- unreadCount: message.conversationId === state.currentConversationId ? 0 : 1,
- };
- conversations.unshift(newConversation);
- }
-
- return { conversations };
- });
- },
-
- /**
- * EN: Create new conversation
- * VI: Tạo cuộc trò chuyện mới
- *
- * @param title - Optional conversation title / Tiêu đề cuộc trò chuyện tùy chọn
- * @returns New conversation ID / ID cuộc trò chuyện mới
- */
- createConversation: (title?: string) => {
- const conversationId = generateId();
- const now = new Date().toISOString();
-
- const newConversation: Conversation = {
- id: conversationId,
- title: title || 'New Conversation / Cuộc trò chuyện mới',
- updatedAt: now,
- createdAt: now,
- unreadCount: 0,
- };
-
- set((state) => ({
- conversations: [newConversation, ...state.conversations],
- currentConversationId: conversationId,
- messages: {
- ...state.messages,
- [conversationId]: [],
- },
- }));
-
- return conversationId;
- },
-
- /**
- * EN: Select conversation and mark messages as read
- * VI: Chọn cuộc trò chuyện và đánh dấu tin nhắn đã đọc
- *
- * @param conversationId - Conversation ID to select / ID cuộc trò chuyện cần chọn
- */
- selectConversation: (conversationId: string) => {
- set((state) => {
- // EN: Update conversations to clear unread count
- // VI: Cập nhật cuộc trò chuyện để xóa số tin nhắn chưa đọc
- const conversations = state.conversations.map((conv) => {
- if (conv.id === conversationId) {
- return { ...conv, unreadCount: 0 };
- }
- return conv;
- });
-
- return {
- currentConversationId: conversationId,
- conversations,
- };
- });
- },
-
- /**
- * EN: Delete conversation and its messages
- * VI: Xóa cuộc trò chuyện và tin nhắn của nó
- *
- * @param conversationId - Conversation ID to delete / ID cuộc trò chuyện cần xóa
- */
- deleteConversation: (conversationId: string) => {
- set((state) => {
- const conversations = state.conversations.filter((c) => c.id !== conversationId);
- const messages = { ...state.messages };
- delete messages[conversationId];
-
- // EN: Clear current conversation if it was deleted
- // VI: Xóa cuộc trò chuyện hiện tại nếu nó bị xóa
- const currentConversationId = state.currentConversationId === conversationId
- ? null
- : state.currentConversationId;
-
- return {
- conversations,
- messages,
- currentConversationId,
- };
- });
- },
-
- /**
- * EN: Update message status
- * VI: Cập nhật trạng thái tin nhắn
- *
- * @param messageId - Message ID / ID tin nhắn
- * @param conversationId - Conversation ID / ID cuộc trò chuyện
- * @param status - New status / Trạng thái mới
- */
- updateMessageStatus: (messageId: string, conversationId: string, status: MessageStatus) => {
- set((state) => {
- const messages = state.messages[conversationId] || [];
- const updatedMessages = messages.map((msg) => {
- if (msg.id === messageId) {
- return {
- ...msg,
- status,
- updatedAt: new Date().toISOString(),
- };
- }
- return msg;
- });
-
- return {
- messages: {
- ...state.messages,
- [conversationId]: updatedMessages,
- },
- };
- });
- },
-
- /**
- * EN: Set typing indicator for user
- * VI: Đặt chỉ báo đang gõ cho người dùng
- *
- * @param userId - User ID / ID người dùng
- * @param isTyping - Typing state / Trạng thái đang gõ
- */
- setTyping: (userId: string, isTyping: boolean) => {
- set((state) => ({
- typingUsers: {
- ...state.typingUsers,
- [userId]: isTyping,
- },
- }));
- },
-
- /**
- * EN: Clear error message
- * VI: Xóa thông báo lỗi
- */
- clearError: () => {
- set({ error: null });
- },
- }),
- {
- // EN: Persist chat state to localStorage
- // VI: Persist trạng thái chat vào localStorage
- name: 'chat-storage',
- // EN: Only persist conversations and messages, exclude WebSocket client and connection state
- // VI: Chỉ persist conversations và messages, loại trừ WebSocket client và connection state
- partialize: (state) => ({
- conversations: state.conversations,
- messages: state.messages,
- currentConversationId: state.currentConversationId,
- }),
- }
- )
-);
diff --git a/apps/web-client/src/stores/users-store.ts b/apps/web-client/src/stores/users-store.ts
deleted file mode 100644
index c26f50e8..00000000
--- a/apps/web-client/src/stores/users-store.ts
+++ /dev/null
@@ -1,317 +0,0 @@
-import { UserResponse, CreateUserDto, UpdateUserDto, Role } from '@goodgo/types';
-import { create } from 'zustand';
-import { devtools } from 'zustand/middleware';
-
-import {
- getUsers,
- getUser,
- createUser,
- updateUser,
- deleteUser,
- bulkDeleteUsers,
- bulkUpdateUserRoles,
- GetUsersParams,
- GetUsersResponse
-} from '../lib/api/users';
-
-/**
- * EN: Users store state interface
- * VI: Interface trạng thái users store
- */
-interface UsersState {
- /** EN: Paginated users list / VI: Danh sách users phân trang */
- users: UserResponse[];
- /** EN: Single user data for detail view / VI: Dữ liệu user đơn lẻ cho view chi tiết */
- currentUser: UserResponse | null;
- /** EN: Pagination metadata / VI: Metadata phân trang */
- pagination: GetUsersResponse['pagination'] | null;
- /** EN: Loading state for list operations / VI: Trạng thái loading cho operations list */
- isLoading: boolean;
- /** EN: Loading state for single user operations / VI: Trạng thái loading cho operations single user */
- isLoadingUser: boolean;
- /** EN: Error message if any operation fails / VI: Thông báo lỗi nếu có operation thất bại */
- error: string | null;
-
- /** EN: Fetch paginated users list / VI: Lấy danh sách users phân trang */
- fetchUsers: (params?: GetUsersParams) => Promise;
- /** EN: Fetch single user by ID / VI: Lấy user đơn lẻ theo ID */
- fetchUser: (id: string) => Promise;
- /** EN: Create new user / VI: Tạo user mới */
- createUser: (payload: CreateUserDto) => Promise;
- /** EN: Update existing user / VI: Cập nhật user hiện có */
- updateUser: (id: string, payload: UpdateUserDto) => Promise;
- /** EN: Delete user by ID / VI: Xóa user theo ID */
- deleteUser: (id: string) => Promise;
- /** EN: Bulk delete multiple users / VI: Xóa nhiều users cùng lúc */
- bulkDeleteUsers: (ids: string[]) => Promise;
- /** EN: Bulk update user roles / VI: Cập nhật vai trò cho nhiều users */
- bulkUpdateUserRoles: (updates: Array<{ id: string; role: Role }>) => Promise;
- /** EN: Clear current user data / VI: Xóa dữ liệu current user */
- clearCurrentUser: () => void;
- /** EN: Clear error state / VI: Xóa trạng thái lỗi */
- clearError: () => void;
- /** EN: Reset store to initial state / VI: Reset store về trạng thái ban đầu */
- reset: () => void;
-}
-
-/**
- * EN: Initial state for users store
- * VI: Trạng thái ban đầu cho users store
- */
-const initialState = {
- users: [],
- currentUser: null,
- pagination: null,
- isLoading: false,
- isLoadingUser: false,
- error: null,
-};
-
-/**
- * EN: Zustand store for users state management
- * VI: Zustand store để quản lý trạng thái users
- *
- * Features:
- * - Paginated users list management
- * - Single user operations (CRUD)
- * - Bulk operations support
- * - Loading and error states
- * - DevTools integration for debugging
- */
-export const useUsersStore = create()(
- devtools(
- (set, get) => ({
- ...initialState,
-
- /**
- * EN: Fetch paginated users list
- * VI: Lấy danh sách users phân trang
- */
- fetchUsers: async (params = {}) => {
- set({ isLoading: true, error: null });
-
- try {
- const response = await getUsers(params);
- set({
- users: response.data,
- pagination: response.pagination,
- isLoading: false,
- });
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to fetch users';
- set({
- error: errorMessage,
- isLoading: false,
- users: [],
- pagination: null,
- });
- throw error;
- }
- },
-
- /**
- * EN: Fetch single user by ID
- * VI: Lấy user đơn lẻ theo ID
- */
- fetchUser: async (id: string) => {
- set({ isLoadingUser: true, error: null });
-
- try {
- const user = await getUser(id);
- set({
- currentUser: user,
- isLoadingUser: false,
- });
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to fetch user';
- set({
- error: errorMessage,
- isLoadingUser: false,
- currentUser: null,
- });
- throw error;
- }
- },
-
- /**
- * EN: Create new user
- * VI: Tạo user mới
- */
- createUser: async (payload: CreateUserDto) => {
- set({ isLoadingUser: true, error: null });
-
- try {
- const newUser = await createUser(payload);
-
- // Add to users list if we have one
- const { users, pagination } = get();
- if (users.length > 0 && pagination) {
- set({
- users: [newUser, ...users.slice(0, -1)], // Add to beginning, remove last to maintain page size
- isLoadingUser: false,
- });
- } else {
- set({ isLoadingUser: false });
- }
-
- return newUser;
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to create user';
- set({
- error: errorMessage,
- isLoadingUser: false,
- });
- throw error;
- }
- },
-
- /**
- * EN: Update existing user
- * VI: Cập nhật user hiện có
- */
- updateUser: async (id: string, payload: UpdateUserDto) => {
- set({ isLoadingUser: true, error: null });
-
- try {
- const updatedUser = await updateUser(id, payload);
-
- // Update in users list if present
- const { users } = get();
- const updatedUsers = users.map(user =>
- user.id === id ? updatedUser : user
- );
-
- set({
- users: updatedUsers,
- currentUser: updatedUser,
- isLoadingUser: false,
- });
-
- return updatedUser;
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to update user';
- set({
- error: errorMessage,
- isLoadingUser: false,
- });
- throw error;
- }
- },
-
- /**
- * EN: Delete user by ID
- * VI: Xóa user theo ID
- */
- deleteUser: async (id: string) => {
- set({ isLoadingUser: true, error: null });
-
- try {
- await deleteUser(id);
-
- // Remove from users list
- const { users } = get();
- const filteredUsers = users.filter(user => user.id !== id);
-
- set({
- users: filteredUsers,
- currentUser: null,
- isLoadingUser: false,
- });
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to delete user';
- set({
- error: errorMessage,
- isLoadingUser: false,
- });
- throw error;
- }
- },
-
- /**
- * EN: Bulk delete multiple users
- * VI: Xóa nhiều users cùng lúc
- */
- bulkDeleteUsers: async (ids: string[]) => {
- set({ isLoading: true, error: null });
-
- try {
- await bulkDeleteUsers(ids);
-
- // Remove from users list
- const { users } = get();
- const filteredUsers = users.filter(user => !ids.includes(user.id));
-
- set({
- users: filteredUsers,
- isLoading: false,
- });
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to bulk delete users';
- set({
- error: errorMessage,
- isLoading: false,
- });
- throw error;
- }
- },
-
- /**
- * EN: Bulk update user roles
- * VI: Cập nhật vai trò cho nhiều users
- */
- bulkUpdateUserRoles: async (updates: Array<{ id: string; role: Role }>) => {
- set({ isLoading: true, error: null });
-
- try {
- await bulkUpdateUserRoles(updates);
-
- // Update in users list
- const { users } = get();
- const updatedUsers = users.map(user => {
- const update = updates.find(u => u.id === user.id);
- return update ? { ...user, role: update.role } : user;
- });
-
- set({
- users: updatedUsers,
- isLoading: false,
- });
- } catch (error: any) {
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to bulk update user roles';
- set({
- error: errorMessage,
- isLoading: false,
- });
- throw error;
- }
- },
-
- /**
- * EN: Clear current user data
- * VI: Xóa dữ liệu current user
- */
- clearCurrentUser: () => {
- set({ currentUser: null });
- },
-
- /**
- * EN: Clear error state
- * VI: Xóa trạng thái lỗi
- */
- clearError: () => {
- set({ error: null });
- },
-
- /**
- * EN: Reset store to initial state
- * VI: Reset store về trạng thái ban đầu
- */
- reset: () => {
- set(initialState);
- },
- }),
- {
- name: 'users-store', // DevTools store name
- }
- )
-);
\ No newline at end of file
diff --git a/apps/web-client/src/stories/Button.stories.ts b/apps/web-client/src/stories/Button.stories.ts
deleted file mode 100644
index 89e55d68..00000000
--- a/apps/web-client/src/stories/Button.stories.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite';
-
-import { fn } from 'storybook/test';
-
-import { Button } from './Button';
-
-// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
-const meta = {
- title: 'Example/Button',
- component: Button,
- parameters: {
- // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
- layout: 'centered',
- },
- // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
- tags: ['autodocs'],
- // More on argTypes: https://storybook.js.org/docs/api/argtypes
- argTypes: {
- backgroundColor: { control: 'color' },
- },
- // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
- args: { onClick: fn() },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
-export const Primary: Story = {
- args: {
- primary: true,
- label: 'Button',
- },
-};
-
-export const Secondary: Story = {
- args: {
- label: 'Button',
- },
-};
-
-export const Large: Story = {
- args: {
- size: 'large',
- label: 'Button',
- },
-};
-
-export const Small: Story = {
- args: {
- size: 'small',
- label: 'Button',
- },
-};
diff --git a/apps/web-client/src/stories/Button.tsx b/apps/web-client/src/stories/Button.tsx
deleted file mode 100644
index d96916cc..00000000
--- a/apps/web-client/src/stories/Button.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import './button.css';
-
-export interface ButtonProps {
- /** Is this the principal call to action on the page? */
- primary?: boolean;
- /** What background color to use */
- backgroundColor?: string;
- /** How large should the button be? */
- size?: 'small' | 'medium' | 'large';
- /** Button contents */
- label: string;
- /** Optional click handler */
- onClick?: () => void;
-}
-
-/** Primary UI component for user interaction */
-export const Button = ({
- primary = false,
- size = 'medium',
- backgroundColor,
- label,
- ...props
-}: ButtonProps) => {
- const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
- return (
-
- {label}
-
-
- );
-};
diff --git a/apps/web-client/src/stories/Configure.mdx b/apps/web-client/src/stories/Configure.mdx
deleted file mode 100644
index 70fcc2a9..00000000
--- a/apps/web-client/src/stories/Configure.mdx
+++ /dev/null
@@ -1,446 +0,0 @@
-import { Meta } from "@storybook/addon-docs/blocks";
-import Image from "next/image";
-
-import Github from "./assets/github.svg";
-import Discord from "./assets/discord.svg";
-import Youtube from "./assets/youtube.svg";
-import Tutorials from "./assets/tutorials.svg";
-import Styling from "./assets/styling.png";
-import Context from "./assets/context.png";
-import Assets from "./assets/assets.png";
-import Docs from "./assets/docs.png";
-import Share from "./assets/share.png";
-import FigmaPlugin from "./assets/figma-plugin.png";
-import Testing from "./assets/testing.png";
-import Accessibility from "./assets/accessibility.png";
-import Theming from "./assets/theming.png";
-import AddonLibrary from "./assets/addon-library.png";
-
-export const RightArrow = () =>
-
-
-
-
-
-
-
- # Configure your project
-
- Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
-
-
-
-
-
Add styling and CSS
-
Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
-
Learn more
-
-
-
-
Provide context and mocking
-
Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.
-
Learn more
-
-
-
-
-
Load assets and resources
-
To link static files (like fonts) to your projects and stories, use the
- `staticDirs` configuration option to specify folders to load when
- starting Storybook.
-
Learn more
-
-
-
-
-
-
- # Do more with Storybook
-
- Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
-
-
-
-
-
-
-
Autodocs
-
Auto-generate living,
- interactive reference documentation from your components and stories.
-
Learn more
-
-
-
-
Publish to Chromatic
-
Publish your Storybook to review and collaborate with your entire team.
-
Learn more
-
-
-
-
Figma Plugin
-
Embed your stories into Figma to cross-reference the design and live
- implementation in one place.
-
Learn more
-
-
-
-
Testing
-
Use stories to test a component in all its variations, no matter how
- complex.
-
Learn more
-
-
-
-
Accessibility
-
Automatically test your components for a11y issues as you develop.
-
Learn more
-
-
-
-
Theming
-
Theme Storybook's UI to personalize it to your project.
-
Learn more
-
-
-
-
-
-
-
-
-
- Join our contributors building the future of UI development.
-
-
Star on GitHub
-
-
-
-
-
-
-
diff --git a/apps/web-client/src/stories/Header.stories.ts b/apps/web-client/src/stories/Header.stories.ts
deleted file mode 100644
index 29fff1ec..00000000
--- a/apps/web-client/src/stories/Header.stories.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite';
-
-import { fn } from 'storybook/test';
-
-import { Header } from './Header';
-
-const meta = {
- title: 'Example/Header',
- component: Header,
- // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
- tags: ['autodocs'],
- parameters: {
- // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
- layout: 'fullscreen',
- },
- args: {
- onLogin: fn(),
- onLogout: fn(),
- onCreateAccount: fn(),
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const LoggedIn: Story = {
- args: {
- user: {
- name: 'Jane Doe',
- },
- },
-};
-
-export const LoggedOut: Story = {};
diff --git a/apps/web-client/src/stories/Header.tsx b/apps/web-client/src/stories/Header.tsx
deleted file mode 100644
index d05ed4f6..00000000
--- a/apps/web-client/src/stories/Header.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Button } from './Button';
-import './header.css';
-
-type User = {
- name: string;
-};
-
-export interface HeaderProps {
- user?: User;
- onLogin?: () => void;
- onLogout?: () => void;
- onCreateAccount?: () => void;
-}
-
-export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
-
-);
diff --git a/apps/web-client/src/stories/Page.stories.ts b/apps/web-client/src/stories/Page.stories.ts
deleted file mode 100644
index 44adf028..00000000
--- a/apps/web-client/src/stories/Page.stories.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite';
-
-import { expect, userEvent, within } from 'storybook/test';
-
-import { Page } from './Page';
-
-const meta = {
- title: 'Example/Page',
- component: Page,
- parameters: {
- // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
- layout: 'fullscreen',
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const LoggedOut: Story = {};
-
-// More on component testing: https://storybook.js.org/docs/writing-tests/interaction-testing
-export const LoggedIn: Story = {
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- const loginButton = canvas.getByRole('button', { name: /Log in/i });
- await expect(loginButton).toBeInTheDocument();
- await userEvent.click(loginButton);
- await expect(loginButton).not.toBeInTheDocument();
-
- const logoutButton = canvas.getByRole('button', { name: /Log out/i });
- await expect(logoutButton).toBeInTheDocument();
- },
-};
diff --git a/apps/web-client/src/stories/Page.tsx b/apps/web-client/src/stories/Page.tsx
deleted file mode 100644
index 3c30fcaf..00000000
--- a/apps/web-client/src/stories/Page.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { useState } from 'react';
-
-import { Header } from './Header';
-import './page.css';
-
-type User = {
- name: string;
-};
-
-export const Page: React.FC = () => {
- const [user, setUser] = useState();
-
- return (
-
- setUser({ name: 'Jane Doe' })}
- onLogout={() => setUser(undefined)}
- onCreateAccount={() => setUser({ name: 'Jane Doe' })}
- />
-
-
- Pages in Storybook
-
- We recommend building UIs with a{' '}
-
- component-driven
- {' '}
- process starting with atomic components and ending with pages.
-
-
- Render pages with mock data. This makes it easy to build and review page states without
- needing to navigate to them in your app. Here are some handy patterns for managing page
- data in Storybook:
-
-
-
- Use a higher-level connected component. Storybook helps you compose such data from the
- "args" of child component stories
-
-
- Assemble data in the page component from your services. You can mock these services out
- using Storybook.
-
-
-
- Get a guided tutorial on component-driven development at{' '}
-
- Storybook tutorials
-
- . Read more in the{' '}
-
- docs
-
- .
-
-
-
Tip Adjust the width of the canvas with the{' '}
-
-
-
-
-
- Viewports addon in the toolbar
-
-
-
- );
-};
diff --git a/apps/web-client/src/stories/assets/accessibility.png b/apps/web-client/src/stories/assets/accessibility.png
deleted file mode 100644
index 6ffe6fea..00000000
Binary files a/apps/web-client/src/stories/assets/accessibility.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/accessibility.svg b/apps/web-client/src/stories/assets/accessibility.svg
deleted file mode 100644
index 107e93f8..00000000
--- a/apps/web-client/src/stories/assets/accessibility.svg
+++ /dev/null
@@ -1 +0,0 @@
-Accessibility
\ No newline at end of file
diff --git a/apps/web-client/src/stories/assets/addon-library.png b/apps/web-client/src/stories/assets/addon-library.png
deleted file mode 100644
index 95deb38a..00000000
Binary files a/apps/web-client/src/stories/assets/addon-library.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/assets.png b/apps/web-client/src/stories/assets/assets.png
deleted file mode 100644
index cfba6817..00000000
Binary files a/apps/web-client/src/stories/assets/assets.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/avif-test-image.avif b/apps/web-client/src/stories/assets/avif-test-image.avif
deleted file mode 100644
index 530709bc..00000000
Binary files a/apps/web-client/src/stories/assets/avif-test-image.avif and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/context.png b/apps/web-client/src/stories/assets/context.png
deleted file mode 100644
index e5cd249a..00000000
Binary files a/apps/web-client/src/stories/assets/context.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/discord.svg b/apps/web-client/src/stories/assets/discord.svg
deleted file mode 100644
index d638958b..00000000
--- a/apps/web-client/src/stories/assets/discord.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web-client/src/stories/assets/docs.png b/apps/web-client/src/stories/assets/docs.png
deleted file mode 100644
index a749629d..00000000
Binary files a/apps/web-client/src/stories/assets/docs.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/figma-plugin.png b/apps/web-client/src/stories/assets/figma-plugin.png
deleted file mode 100644
index 8f79b08c..00000000
Binary files a/apps/web-client/src/stories/assets/figma-plugin.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/github.svg b/apps/web-client/src/stories/assets/github.svg
deleted file mode 100644
index dc513528..00000000
--- a/apps/web-client/src/stories/assets/github.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web-client/src/stories/assets/share.png b/apps/web-client/src/stories/assets/share.png
deleted file mode 100644
index 8097a370..00000000
Binary files a/apps/web-client/src/stories/assets/share.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/styling.png b/apps/web-client/src/stories/assets/styling.png
deleted file mode 100644
index d341e826..00000000
Binary files a/apps/web-client/src/stories/assets/styling.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/testing.png b/apps/web-client/src/stories/assets/testing.png
deleted file mode 100644
index d4ac39a0..00000000
Binary files a/apps/web-client/src/stories/assets/testing.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/theming.png b/apps/web-client/src/stories/assets/theming.png
deleted file mode 100644
index 1535eb9b..00000000
Binary files a/apps/web-client/src/stories/assets/theming.png and /dev/null differ
diff --git a/apps/web-client/src/stories/assets/tutorials.svg b/apps/web-client/src/stories/assets/tutorials.svg
deleted file mode 100644
index b492a9c6..00000000
--- a/apps/web-client/src/stories/assets/tutorials.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web-client/src/stories/assets/youtube.svg b/apps/web-client/src/stories/assets/youtube.svg
deleted file mode 100644
index a7515d7e..00000000
--- a/apps/web-client/src/stories/assets/youtube.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web-client/src/stories/button.css b/apps/web-client/src/stories/button.css
deleted file mode 100644
index 4e3620b0..00000000
--- a/apps/web-client/src/stories/button.css
+++ /dev/null
@@ -1,30 +0,0 @@
-.storybook-button {
- display: inline-block;
- cursor: pointer;
- border: 0;
- border-radius: 3em;
- font-weight: 700;
- line-height: 1;
- font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-}
-.storybook-button--primary {
- background-color: #555ab9;
- color: white;
-}
-.storybook-button--secondary {
- box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
- background-color: transparent;
- color: #333;
-}
-.storybook-button--small {
- padding: 10px 16px;
- font-size: 12px;
-}
-.storybook-button--medium {
- padding: 11px 20px;
- font-size: 14px;
-}
-.storybook-button--large {
- padding: 12px 24px;
- font-size: 16px;
-}
diff --git a/apps/web-client/src/stories/header.css b/apps/web-client/src/stories/header.css
deleted file mode 100644
index 5efd46c2..00000000
--- a/apps/web-client/src/stories/header.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.storybook-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
- padding: 15px 20px;
- font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-}
-
-.storybook-header svg {
- display: inline-block;
- vertical-align: top;
-}
-
-.storybook-header h1 {
- display: inline-block;
- vertical-align: top;
- margin: 6px 0 6px 10px;
- font-weight: 700;
- font-size: 20px;
- line-height: 1;
-}
-
-.storybook-header button + button {
- margin-left: 10px;
-}
-
-.storybook-header .welcome {
- margin-right: 10px;
- color: #333;
- font-size: 14px;
-}
diff --git a/apps/web-client/src/stories/page.css b/apps/web-client/src/stories/page.css
deleted file mode 100644
index 77c81d2d..00000000
--- a/apps/web-client/src/stories/page.css
+++ /dev/null
@@ -1,68 +0,0 @@
-.storybook-page {
- margin: 0 auto;
- padding: 48px 20px;
- max-width: 600px;
- color: #333;
- font-size: 14px;
- line-height: 24px;
- font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-}
-
-.storybook-page h2 {
- display: inline-block;
- vertical-align: top;
- margin: 0 0 4px;
- font-weight: 700;
- font-size: 32px;
- line-height: 1;
-}
-
-.storybook-page p {
- margin: 1em 0;
-}
-
-.storybook-page a {
- color: inherit;
-}
-
-.storybook-page ul {
- margin: 1em 0;
- padding-left: 30px;
-}
-
-.storybook-page li {
- margin-bottom: 8px;
-}
-
-.storybook-page .tip {
- display: inline-block;
- vertical-align: top;
- margin-right: 10px;
- border-radius: 1em;
- background: #e7fdd8;
- padding: 4px 12px;
- color: #357a14;
- font-weight: 700;
- font-size: 11px;
- line-height: 12px;
-}
-
-.storybook-page .tip-wrapper {
- margin-top: 40px;
- margin-bottom: 40px;
- font-size: 13px;
- line-height: 20px;
-}
-
-.storybook-page .tip-wrapper svg {
- display: inline-block;
- vertical-align: top;
- margin-top: 3px;
- margin-right: 4px;
- width: 12px;
- height: 12px;
-}
-
-.storybook-page .tip-wrapper svg path {
- fill: #1ea7fd;
-}
diff --git a/apps/web-client/src/styles/glass.css b/apps/web-client/src/styles/glass.css
deleted file mode 100644
index 61fd399d..00000000
--- a/apps/web-client/src/styles/glass.css
+++ /dev/null
@@ -1,369 +0,0 @@
-/**
- * EN: Glassmorphism Utility Classes
- * VI: Các class tiện ích cho Glassmorphism
- *
- * This file contains reusable glassmorphism utility classes following the subtle x.ai aesthetic.
- * These classes combine background, borders, blur, and shadows to create professional glass effects.
- *
- * File này chứa các class tiện ích glassmorphism có thể tái sử dụng theo phong cách x.ai tinh tế.
- * Các class này kết hợp background, borders, blur, và shadows để tạo hiệu ứng glass chuyên nghiệp.
- */
-
-/* ============================================
- EN: Basic Glass Panel
- VI: Glass Panel cơ bản
- ============================================ */
-
-/**
- * Basic glass panel with default settings
- * Suitable for cards, containers, and panels
- */
-.glass-panel {
- background: var(--glass-bg-default);
- backdrop-filter: blur(var(--glass-blur-md));
- -webkit-backdrop-filter: blur(var(--glass-blur-md));
- /* Safari support */
- border: 1px solid var(--glass-border-default);
- box-shadow: var(--glass-shadow-md), var(--glass-shadow-inset);
-}
-
-.glass-panel:hover {
- background: var(--glass-bg-hover);
- border-color: var(--glass-border-hover);
- transition: all var(--duration-fast) var(--ease-snap);
-}
-
-/* ============================================
- EN: Glass Card (with hover animation)
- VI: Glass Card (có animation khi hover)
- ============================================ */
-
-/**
- * Glass card with rounded corners and minimal hover effect
- * X.ai minimalist style - subtle lift only
- */
-.glass-card {
- background: var(--glass-bg-default);
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-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);
- border-radius: var(--radius-lg);
- transition: all var(--duration-fast) var(--ease-snap);
-}
-
-.glass-card:hover {
- border-color: var(--glass-border-hover);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
-}
-
-/**
- * EN: Focus-within state for form groups
- * VI: Trạng thái focus-within cho form groups
- */
-.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);
-}
-
-/* ============================================
- EN: Glass Button
- VI: Glass Button
- ============================================ */
-
-/**
- * Glass button with minimal background - X.ai style
- * Snappy, fast interactions
- */
-.glass-button {
- background: var(--glass-bg-default);
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-backdrop-filter: blur(var(--glass-blur-sm));
- border: 1px solid var(--glass-border-default);
- box-shadow: var(--shadow);
- border-radius: var(--radius-md);
- padding: var(--space-3) var(--space-4);
- transition: all var(--duration-fast) var(--ease-snap);
- cursor: pointer;
- user-select: none;
-}
-
-.glass-button:hover:not(:disabled) {
- background: var(--glass-bg-hover);
- border-color: var(--glass-border-hover);
- transform: translateY(-1px);
-}
-
-.glass-button:active:not(:disabled) {
- background: var(--glass-bg-active);
- transform: scale(var(--active-scale));
-}
-
-.glass-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* ============================================
- EN: Glass Input
- VI: Glass Input
- ============================================ */
-
-/**
- * Glass input field - X.ai minimalist
- * Ultra-minimal blur for maximum text clarity
- */
-.glass-input {
- background: var(--glass-bg-subtle);
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-backdrop-filter: blur(var(--glass-blur-sm));
- border: 1px solid var(--glass-border-subtle);
- transition: all var(--duration-fast) var(--ease-snap);
-}
-
-.glass-input:hover {
- border-color: var(--glass-border-default);
-}
-
-.glass-input:focus {
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1);
- outline: none;
-}
-
-.glass-input::placeholder {
- color: var(--text-tertiary);
-}
-
-/* ============================================
- EN: Solid Input (X.ai Style)
- VI: Solid Input (Phong cách X.ai)
- ============================================ */
-
-/**
- * Solid input field for a more grounded feel
- */
-.solid-input {
- background: var(--bg-secondary);
- border: 1px solid var(--border-primary);
- transition: all var(--duration-fast) var(--ease-snap);
-}
-
-.solid-input:hover {
- border-color: var(--border-secondary);
-}
-
-.solid-input:focus {
- background: var(--bg-primary);
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 3px rgba(29, 155, 240, 0.1);
- outline: none;
-}
-
-/* ============================================
- EN: Glass Modal/Dialog
- VI: Glass Modal/Dialog
- ============================================ */
-
-/**
- * Glass modal - X.ai minimal style
- * Reduced blur for cleaner appearance
- */
-.glass-modal {
- background: rgba(10, 10, 10, 0.95);
- backdrop-filter: blur(var(--glass-blur-lg));
- -webkit-backdrop-filter: blur(var(--glass-blur-lg));
- border: 1px solid var(--glass-border-default);
- box-shadow: var(--shadow-lg);
-}
-
-/* ============================================
- EN: Glass Navigation
- VI: Glass Navigation
- ============================================ */
-
-/**
- * Glass navigation bar - X.ai clean style
- * Minimal blur, clean borders
- */
-.glass-nav {
- background: rgba(0, 0, 0, 0.6);
- backdrop-filter: blur(var(--glass-blur-md)) saturate(150%);
- -webkit-backdrop-filter: blur(var(--glass-blur-md)) saturate(150%);
- border-bottom: 1px solid var(--glass-border-subtle);
-}
-
-/* ============================================
- EN: Glass Sidebar
- VI: Glass Sidebar
- ============================================ */
-
-/**
- * Glass sidebar with medium blur
- * Suitable for side panels, drawers
- */
-.glass-sidebar {
- background: var(--glass-bg-medium);
- backdrop-filter: blur(var(--glass-blur-md));
- -webkit-backdrop-filter: blur(var(--glass-blur-md));
- border-right: 1px solid var(--glass-border-default);
-}
-
-/* ============================================
- EN: Glass Badge/Chip
- VI: Glass Badge/Chip
- ============================================ */
-
-/**
- * Small glass badge or chip
- * Use for tags, labels, status indicators
- */
-.glass-badge {
- background: var(--glass-bg-subtle);
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-backdrop-filter: blur(var(--glass-blur-sm));
- border: 1px solid var(--glass-border-subtle);
- padding: 0.25rem 0.75rem;
- border-radius: var(--radius-full);
- font-size: var(--text-sm);
- transition: all var(--duration-fast) var(--ease-snap);
-}
-
-.glass-badge:hover {
- background: var(--glass-bg-default);
- border-color: var(--glass-border-default);
-}
-
-/* ============================================
- EN: Glass Overlay (for modals/dialogs backdrop)
- VI: Glass Overlay (cho backdrop của modal/dialog)
- ============================================ */
-
-/**
- * Full-screen glass overlay - X.ai minimal
- * Reduced blur for performance
- */
-.glass-overlay {
- background: rgba(0, 0, 0, 0.8);
- backdrop-filter: blur(var(--glass-blur-lg));
- -webkit-backdrop-filter: blur(var(--glass-blur-lg));
-}
-
-/* ============================================
- EN: Glass Dropdown
- VI: Glass Dropdown
- ============================================ */
-
-/**
- * Glass dropdown menu - X.ai minimal
- */
-.glass-dropdown {
- background: var(--glass-bg-medium);
- backdrop-filter: blur(var(--glass-blur-md));
- -webkit-backdrop-filter: blur(var(--glass-blur-md));
- border: 1px solid var(--glass-border-default);
- box-shadow: var(--shadow-lg);
- border-radius: var(--radius-lg);
-}
-
-/* ============================================
- EN: Utility Modifiers
- VI: Các modifier tiện ích
- ============================================ */
-
-/**
- * Extra subtle glass effect
- */
-.glass-subtle {
- background: var(--glass-bg-subtle);
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-backdrop-filter: blur(var(--glass-blur-sm));
- border: 1px solid var(--glass-border-subtle);
-}
-
-/**
- * Strong glass effect (use sparingly)
- */
-.glass-strong {
- background: var(--glass-bg-medium);
- backdrop-filter: blur(var(--glass-blur-lg));
- -webkit-backdrop-filter: blur(var(--glass-blur-lg));
- border: 1px solid var(--glass-border-hover);
- box-shadow: var(--glass-shadow-lg), var(--glass-shadow-inset);
-}
-
-/**
- * No border variant
- */
-.glass-no-border {
- border: none;
-}
-
-/**
- * No shadow variant - prefer this for minimalism
- */
-.glass-no-shadow {
- box-shadow: none;
-}
-
-/* ============================================
- EN: Responsive Variants (Optional)
- VI: Các biến thể responsive (Tùy chọn)
- ============================================ */
-
-/**
- * Reduce glass effects on mobile for performance
- */
-@media (max-width: 768px) {
-
- .glass-card,
- .glass-modal,
- .glass-nav {
- backdrop-filter: blur(var(--glass-blur-sm));
- -webkit-backdrop-filter: blur(var(--glass-blur-sm));
- }
-}
-
-/* ============================================
- EN: Animation Classes
- VI: Các class animation
- ============================================ */
-
-/**
- * Glass appear animation - X.ai snappy
- */
-@keyframes glass-appear {
- from {
- opacity: 0;
- transform: scale(0.98);
- }
-
- to {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-.glass-appear {
- animation: glass-appear var(--duration-fast) var(--ease-snap);
-}
-
-/**
- * Glass slide in from top - X.ai snappy
- */
-@keyframes glass-slide-down {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.glass-slide-down {
- animation: glass-slide-down var(--duration-fast) var(--ease-snap);
-}
\ No newline at end of file
diff --git a/apps/web-client/src/styles/theme.css b/apps/web-client/src/styles/theme.css
deleted file mode 100644
index 6c4ec47d..00000000
--- a/apps/web-client/src/styles/theme.css
+++ /dev/null
@@ -1,460 +0,0 @@
-/**
- * EN: Design System Theme Tokens
- * VI: Các token thiết kế cho Design System
- *
- * This file contains all CSS custom properties (variables) for the design system,
- * including colors, typography, spacing, layout, animations, and more.
- * These tokens are used throughout the application and can be referenced in Tailwind CSS
- * utility classes via the tailwind.config.js configuration.
- *
- * File này chứa tất cả các CSS custom properties (biến) cho design system,
- * bao gồm màu sắc, typography, spacing, layout, animations, và nhiều hơn nữa.
- * Các token này được sử dụng trong toàn bộ ứng dụng và có thể được tham chiếu trong
- * các utility classes của Tailwind CSS thông qua cấu hình tailwind.config.js.
- */
-
-:root {
- /* ============================================
- EN: Color Palette - Dark Mode (Primary Theme)
- VI: Bảng màu - Dark Mode (Theme chính)
- ============================================ */
-
- /* Background Colors - Minimal Monochrome */
- --bg-primary: #000000;
- /* True Black */
- --bg-secondary: #111111;
- /* Near Black - Card/Panel */
- --bg-tertiary: #222222;
- /* Dark Gray */
- --bg-elevated: #333333;
- /* Elevated Gray */
-
- /* Text Colors (Highest Contrast) */
- --text-primary: #FFFFFF;
- /* Pure white */
- --text-secondary: #999999;
- /* Light Gray */
- --text-tertiary: #666666;
- /* Dark Gray */
- --text-muted: #444444;
- /* Muted */
- --text-inverse: #000000;
- /* Black */
-
- /* Brand/Accent Colors - Monochrome */
- --accent-primary: #FFFFFF;
- /* White */
- --accent-primary-hover: #CCCCCC;
- /* Light Gray */
- --accent-primary-light: #333333;
- /* Dark Gray */
- --accent-secondary: #333333;
- /* Dark grey */
-
- /* Functional Colors - Kept as monochrome variants or high contrast */
- --accent-success: #FFFFFF;
- /* Success = White (rely on icons) */
- --accent-warning: #CCCCCC;
- /* Warning = Gray */
- --accent-error: #FFFFFF;
- /* Error = White (rely on icons) */
- --accent-info: #FFFFFF;
- /* Info = White */
-
- /* Chat Specific Colors */
- --chat-user-bubble: #222222;
- --chat-ai-bubble: transparent;
- --chat-user-text: #FFFFFF;
- --chat-ai-text: #DDDDDD;
- --chat-timestamp: #666666;
- --chat-divider: #222222;
-
- /* Border Colors */
- --border-primary: #333333;
- --border-secondary: #444444;
- --border-focus: #FFFFFF;
-
- /* ============================================
- EN: Brand Colors (Monochrome)
- VI: Màu thương hiệu (Đơn sắc)
- ============================================ */
-
- /* Primary Brand Color */
- --brand-primary: #FFFFFF;
- --brand-primary-light: #CCCCCC;
- --brand-primary-dark: #999999;
- --brand-primary-contrast: #000000;
-
- /* Secondary Brand Color */
- --brand-secondary: #666666;
- --brand-secondary-light: #999999;
- --brand-secondary-dark: #333333;
-
- /* ============================================
- EN: Minimal Effects
- VI: Hiệu ứng tối giản
- ============================================ */
-
- /* Glass Backgrounds */
- --glass-bg-subtle: rgba(255, 255, 255, 0.02);
- --glass-bg-default: rgba(255, 255, 255, 0.05);
- --glass-bg-medium: rgba(255, 255, 255, 0.08);
- --glass-bg-hover: rgba(255, 255, 255, 0.1);
- --glass-bg-active: rgba(255, 255, 255, 0.15);
-
- /* Glass Borders */
- --glass-border-subtle: rgba(255, 255, 255, 0.1);
- --glass-border-default: rgba(255, 255, 255, 0.15);
- --glass-border-hover: rgba(255, 255, 255, 0.3);
- --glass-border-focus: rgba(255, 255, 255, 0.8);
-
- /* Shadows */
- --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.8);
- --glass-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.9);
- --glass-shadow-lg: 0 8px 24px rgba(0, 0, 0, 1.0);
- --glass-shadow-inset: inset 0 1px 1px rgba(255, 255, 255, 0.1);
-
- --interactive-glass-rest: rgba(255, 255, 255, 0.05);
- --interactive-glass-hover: rgba(255, 255, 255, 0.1);
- --interactive-glass-active: rgba(255, 255, 255, 0.15);
- --interactive-glass-disabled: rgba(255, 255, 255, 0.02);
-
- /* Legacy support - keeping old variables for backward compatibility */
- --glass-bg: var(--glass-bg-default);
- --glass-border: var(--glass-border-default);
- --glass-blur: var(--glass-blur-md);
-
- /* ============================================
- EN: Removed - Extended Shadows (X.ai Minimalist)
- VI: Đã xóa - Extended Shadows (X.ai Minimalist)
- ============================================ */
- /* Removed brand shadows for minimalist approach */
- /* Use --shadow or --shadow-lg instead */
-
- /* ============================================
- EN: Light Mode Colors (Secondary Theme)
- VI: Màu sắc cho Light Mode (Theme phụ)
- ============================================ */
- --bg-primary-light: #FFFFFF;
- --bg-secondary-light: #FBFBFD;
- /* Apple Gray */
- --bg-tertiary-light: #F5F5F7;
- --text-primary-light: #1D1D1F;
- /* Apple Black */
- --text-secondary-light: #86868B;
- /* Apple Gray Text */
- --border-primary-light: #D2D2D7;
-
- /* ============================================
- EN: Typography
- VI: Kiểu chữ
- ============================================ */
-
- /* Font Stack / Bộ font */
- --font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- --font-mono: "JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
-
- /* Display Font - For hero titles (48px+) */
- --font-display: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-
- /* Heading Font - For section headings (24-36px) */
- --font-heading: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-
- /* Type Scale - Clean minimal approach */
- --text-6xl: 3.5rem;
- /* 56px - Hero titles */
- --text-5xl: 2.75rem;
- /* 44px - Page titles */
- --text-4xl: 2.25rem;
- /* 36px - Section headers */
- --text-3xl: 1.75rem;
- /* 28px - Card headers */
- --text-2xl: 1.5rem;
- /* 24px - Large body */
- --text-xl: 1.25rem;
- /* 20px - Emphasized text */
- --text-lg: 1.125rem;
- /* 18px - Large body */
- --text-base: 1rem;
- /* 16px - Default body */
- --text-sm: 0.875rem;
- /* 14px - Small text */
- --text-xs: 0.75rem;
- /* 12px - Captions */
-
- /* Line Heights / Chiều cao dòng */
- --leading-none: 1;
- --leading-tight: 1.1;
- --leading-snug: 1.2;
- --leading-normal: 1.5;
- --leading-relaxed: 1.625;
- --leading-loose: 2;
-
- /* Font Weights - X.ai Minimalist (Bolder Impact) */
- --font-thin: 100;
- --font-extralight: 200;
- --font-light: 300;
- /* Light text */
- --font-normal: 400;
- /* Body text */
- --font-medium: 500;
- /* Emphasized */
- --font-semibold: 600;
- /* Headings */
- --font-bold: 700;
- /* Bold - stronger impact */
- --font-extrabold: 800;
- /* Extra bold - hero text */
- --font-black: 900;
- /* Black - maximum impact for titles */
-
- /* Letter Spacing - Clean Minimalist Look */
- --tracking-tighter: -0.04em;
- /* Very tight - for large display text */
- --tracking-tight: -0.02em;
- /* Tight - for headings */
- --tracking-normal: 0;
- /* Normal */
- --tracking-wide: 0.02em;
- /* Wide - for small caps */
- --tracking-wider: 0.04em;
- /* Wider - for emphasis */
-
- /* ============================================
- EN: Spacing & Layout
- VI: Khoảng cách & Bố cục
- ============================================ */
-
- /* Base Unit: 4px (0.25rem) / Đơn vị cơ sở: 4px (0.25rem) */
- --space-0: 0;
- --space-1: 0.25rem;
- /* 4px */
- --space-2: 0.5rem;
- /* 8px */
- --space-3: 0.75rem;
- /* 12px */
- --space-4: 1rem;
- /* 16px */
- --space-5: 1.25rem;
- /* 20px */
- --space-6: 1.5rem;
- /* 24px */
- --space-8: 2rem;
- /* 32px */
- --space-10: 2.5rem;
- /* 40px */
- --space-12: 3rem;
- /* 48px */
- --space-16: 4rem;
- /* 64px */
- --space-20: 5rem;
- /* 80px */
-
- /* Container Widths / Chiều rộng container */
- --container-sm: 640px;
- /* Small devices */
- --container-md: 768px;
- /* Medium devices */
- --container-lg: 1024px;
- /* Large devices */
- --container-xl: 1280px;
- /* Extra large */
- --container-2xl: 1536px;
- /* 2X large */
- --chat-max-width: 800px;
- /* Max width for chat messages */
- --sidebar-width: 280px;
- /* Conversation history sidebar */
-
- /* Mobile Layout / Layout Mobile */
- --mobile-header-height: 56px;
- /* Standard mobile header height */
- --mobile-bottom-nav-height: 64px;
- /* iOS/Android bottom nav height */
- --mobile-safe-area-top: env(safe-area-inset-top);
- /* iOS notch safe area */
- --mobile-safe-area-bottom: env(safe-area-inset-bottom);
- /* iOS home indicator safe area */
-
- /* Border Radius / Bo góc */
- --radius-sm: 2px;
- /* Small elements - sharp */
- --radius-md: 4px;
- /* Buttons, inputs - sharp */
- --radius-lg: 8px;
- /* Cards - minimal roundness */
- --radius-xl: 12px;
- /* Large cards */
- --radius-2xl: 16px;
- /* Modals */
- --radius-full: 9999px;
- /* Full round - Avatars, pills */
-
- /* ============================================
- EN: Shadows - X.ai Minimalist (Ultra Subtle)
- VI: Đổ bóng - X.ai Minimalist (Cực kỳ tinh tế)
- ============================================ */
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
- /* Default shadow - subtle */
- --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.6);
- /* Large shadow - for modals only */
-
- /* Legacy support - mapping to new shadows */
- --shadow-sm: var(--shadow);
- --shadow-md: var(--shadow);
- --shadow-xl: var(--shadow-lg);
- --shadow-glow: 0 0 8px rgba(255, 255, 255, 0.05);
- /* Minimal glow for focus */
-
- /* Brand shadows for brand buttons */
- --shadow-brand: 0 4px 16px rgba(29, 155, 240, 0.3);
- /* Brand shadow with X.ai blue glow */
- --shadow-brand-lg: 0 8px 24px rgba(29, 155, 240, 0.4);
- /* Large brand shadow */
-
- /* ============================================
- EN: Grid System & Breakpoints
- VI: Hệ thống lưới & Điểm ngắt
- ============================================ */
- --screen-sm: 640px;
- /* Mobile landscape */
- --screen-md: 768px;
- /* Tablet */
- --screen-lg: 1024px;
- /* Desktop */
- --screen-xl: 1280px;
- /* Large desktop */
- --screen-2xl: 1536px;
- /* Extra large desktop */
-
- /* ============================================
- EN: Animation & Transitions
- VI: Animation & Chuyển tiếp
- ============================================ */
-
- /* Timing Functions - X.ai Minimalist (Simplified) */
- --ease-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94);
- /* Smooth and natural - primary easing */
- --ease-snap: cubic-bezier(0.4, 0, 0.2, 1);
- /* Snappy - for quick interactions */
-
- /* Legacy support - mapping to new easing */
- --ease-in: var(--ease-snap);
- --ease-out: var(--ease-snap);
- --ease-in-out: var(--ease-smooth);
- --motion-ease-smooth: var(--ease-smooth);
- --motion-ease-glide: var(--ease-snap);
-
- /* Duration - X.ai Minimalist (Faster, Snappier) */
- --duration-instant: 100ms;
- /* Instant feedback */
- --duration-fast: 150ms;
- /* Hover effects */
- --duration-normal: 200ms;
- /* Default transitions - FASTER */
- --duration-slow: 300ms;
- /* Complex animations - FASTER */
-
- /* Legacy support - mapping to new durations */
- --duration-slower: var(--duration-slow);
- --motion-duration-instant: var(--duration-instant);
- --motion-duration-quick: var(--duration-fast);
- --motion-duration-normal: var(--duration-normal);
- --motion-duration-smooth: var(--duration-slow);
-
- /* ============================================
- EN: Interactive States - X.ai Minimalist
- VI: Trạng thái tương tác - X.ai Minimalist
- ============================================ */
-
- /* Removed bounce/elastic - too playful for minimalism */
-
- /* Hover Scale - Minimal movement */
- --hover-scale-sm: 1.01;
- /* Barely noticeable */
- --hover-scale-md: 1.02;
- /* Subtle */
-
- /* Active Scale - For pressed states */
- --active-scale: 0.99;
- /* Minimal press feedback */
-}
-
-/* ============================================
- EN: Light Mode Theme Overrides
- VI: Ghi đè theme cho Light Mode
- ============================================ */
-@media (prefers-color-scheme: light) {
- :root {
- --bg-primary: var(--bg-primary-light);
- --bg-secondary: var(--bg-secondary-light);
- --bg-tertiary: var(--bg-tertiary-light);
- --text-primary: var(--text-primary-light);
- --text-secondary: var(--text-secondary-light);
- --border-primary: var(--border-primary-light);
- }
-}
-
-/* ============================================
- EN: Dark Mode Theme (Explicit)
- VI: Theme Dark Mode (Rõ ràng)
- ============================================ */
-[data-theme="dark"],
-.dark {
- --bg-primary: #000000;
- --bg-secondary: #111111;
- --bg-tertiary: #222222;
- --bg-elevated: #333333;
- --text-primary: #FFFFFF;
- --text-secondary: #999999;
- --text-tertiary: #666666;
- --text-muted: #444444;
- --border-primary: #333333;
- --border-secondary: #444444;
- --border-focus: #FFFFFF;
- --accent-primary: #FFFFFF;
- --accent-primary-hover: #CCCCCC;
-}
-
-/* ============================================
- EN: Light Mode Theme (Explicit)
- VI: Theme Light Mode (Rõ ràng)
- ============================================ */
-[data-theme="light"],
-.light {
- --bg-primary: #FFFFFF;
- --bg-secondary: #F5F5F5;
- --bg-tertiary: #E5E5E5;
- --bg-elevated: #DDDDDD;
- --text-primary: #000000;
- --text-secondary: #333333;
- --text-tertiary: #666666;
- --text-muted: #999999;
- --border-primary: #DDDDDD;
- --border-secondary: #CCCCCC;
- --border-focus: #000000;
-
- --accent-primary: #000000;
- --accent-primary-hover: #333333;
-
- --brand-primary: #000000;
- --brand-primary-light: #333333;
- --brand-primary-dark: #000000;
- --brand-primary-contrast: #FFFFFF;
-
- /* Glass effects adapted for light */
- --glass-bg-subtle: rgba(0, 0, 0, 0.02);
- --glass-bg-default: rgba(0, 0, 0, 0.03);
- --glass-bg-medium: rgba(0, 0, 0, 0.05);
- --glass-bg-hover: rgba(0, 0, 0, 0.08);
- --glass-bg-active: rgba(0, 0, 0, 0.12);
-
- --glass-border-subtle: rgba(0, 0, 0, 0.05);
- --glass-border-default: rgba(0, 0, 0, 0.1);
- --glass-border-hover: rgba(0, 0, 0, 0.2);
- --glass-border-focus: rgba(0, 0, 0, 0.8);
-
- --glass-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.05);
- --glass-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.1);
- --glass-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);
-}
\ No newline at end of file
diff --git a/apps/web-client/src/test/setup.ts b/apps/web-client/src/test/setup.ts
deleted file mode 100644
index 2f58ed4f..00000000
--- a/apps/web-client/src/test/setup.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * EN: Test setup file for Vitest
- * VI: File setup test cho Vitest
- *
- * This file runs before each test file
- * File này chạy trước mỗi test file
- */
-import { expect, afterEach } from 'vitest';
-import { cleanup } from '@testing-library/react';
-import '@testing-library/jest-dom/vitest';
-
-// EN: Cleanup after each test / VI: Dọn dẹp sau mỗi test
-afterEach(() => {
- cleanup();
-});
diff --git a/apps/web-client/tailwind.config.js b/apps/web-client/tailwind.config.js
deleted file mode 100644
index 865ea3bf..00000000
--- a/apps/web-client/tailwind.config.js
+++ /dev/null
@@ -1,312 +0,0 @@
-/**
- * EN: Tailwind CSS 4 Configuration
- * VI: Cấu hình Tailwind CSS 4
- *
- * Note: Tailwind CSS 4 uses CSS-first configuration with @theme directive in CSS files.
- * This config file extends the theme with additional utility classes based on CSS variables.
- * The main theme tokens are defined in src/styles/theme.css.
- *
- * Lưu ý: Tailwind CSS 4 sử dụng cấu hình CSS-first với @theme directive trong file CSS.
- * File config này mở rộng theme với các utility classes bổ sung dựa trên CSS variables.
- * Các theme tokens chính được định nghĩa trong src/styles/theme.css.
- */
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
- './src/**/*.{js,ts,jsx,tsx,mdx}',
- './src/stories/**/*.{js,ts,jsx,tsx,mdx}',
- ],
- darkMode: ['class', '[data-theme="dark"]'],
- theme: {
- extend: {
- // EN: Colors from CSS variables (theme.css)
- // VI: Màu sắc từ CSS variables (theme.css)
- colors: {
- bg: {
- primary: 'var(--bg-primary)',
- secondary: 'var(--bg-secondary)',
- tertiary: 'var(--bg-tertiary)',
- elevated: 'var(--bg-elevated)',
- },
- text: {
- primary: 'var(--text-primary)',
- secondary: 'var(--text-secondary)',
- tertiary: 'var(--text-tertiary)',
- inverse: 'var(--text-inverse)',
- },
- accent: {
- primary: 'var(--accent-primary)',
- secondary: 'var(--accent-secondary)',
- success: 'var(--accent-success)',
- warning: 'var(--accent-warning)',
- error: 'var(--accent-error)',
- info: 'var(--accent-info)',
- },
- chat: {
- 'user-bubble': 'var(--chat-user-bubble)',
- 'ai-bubble': 'var(--chat-ai-bubble)',
- 'user-text': 'var(--chat-user-text)',
- 'ai-text': 'var(--chat-ai-text)',
- timestamp: 'var(--chat-timestamp)',
- divider: 'var(--chat-divider)',
- },
- border: {
- primary: 'var(--border-primary)',
- secondary: 'var(--border-secondary)',
- focus: 'var(--border-focus)',
- },
- // EN: Brand colors for easy access / VI: Màu thương hiệu dễ sử dụng
- brand: {
- primary: {
- DEFAULT: 'var(--brand-primary)',
- light: 'var(--brand-primary-light)',
- dark: 'var(--brand-primary-dark)',
- contrast: 'var(--brand-primary-contrast)',
- },
- secondary: {
- DEFAULT: 'var(--brand-secondary)',
- light: 'var(--brand-secondary-light)',
- dark: 'var(--brand-secondary-dark)',
- },
- accent: {
- DEFAULT: 'var(--brand-accent)',
- light: 'var(--brand-accent-light)',
- dark: 'var(--brand-accent-dark)',
- },
- },
- // EN: Glassmorphism utilities (subtle x.ai style)
- // VI: Utilities glassmorphism (phong cách x.ai tinh tế)
- glass: {
- // Backgrounds
- bg: 'var(--glass-bg-default)',
- 'bg-subtle': 'var(--glass-bg-subtle)',
- 'bg-default': 'var(--glass-bg-default)',
- 'bg-medium': 'var(--glass-bg-medium)',
- 'bg-hover': 'var(--glass-bg-hover)',
- 'bg-active': 'var(--glass-bg-active)',
- // Borders
- border: 'var(--glass-border-default)',
- 'border-subtle': 'var(--glass-border-subtle)',
- 'border-default': 'var(--glass-border-default)',
- 'border-hover': 'var(--glass-border-hover)',
- 'border-focus': 'var(--glass-border-focus)',
- },
- },
- // EN: Glass background colors
- // VI: Màu nền glass
- backgroundColor: {
- 'glass-subtle': 'var(--glass-bg-subtle)',
- 'glass': 'var(--glass-bg-default)',
- 'glass-medium': 'var(--glass-bg-medium)',
- 'glass-hover': 'var(--glass-bg-hover)',
- 'glass-active': 'var(--glass-bg-active)',
- },
- // EN: Glass border colors
- // VI: Màu viền glass
- borderColor: {
- 'glass-subtle': 'var(--glass-border-subtle)',
- 'glass': 'var(--glass-border-default)',
- 'glass-hover': 'var(--glass-border-hover)',
- 'glass-focus': 'var(--glass-border-focus)',
- },
- // EN: Font families from CSS variables
- // VI: Font families từ CSS variables
- fontFamily: {
- sans: ['var(--font-sans)', 'sans-serif'],
- mono: ['var(--font-mono)', 'monospace'],
- },
- // EN: Font sizes from CSS variables (for utility classes)
- // VI: Kích thước chữ từ CSS variables (cho utility classes)
- fontSize: {
- '6xl': ['var(--text-6xl)', { lineHeight: '1' }],
- '5xl': ['var(--text-5xl)', { lineHeight: '1' }],
- '4xl': ['var(--text-4xl)', { lineHeight: '1.1' }],
- '3xl': ['var(--text-3xl)', { lineHeight: '1.2' }],
- '2xl': ['var(--text-2xl)', { lineHeight: '1.3' }],
- 'xl': ['var(--text-xl)', { lineHeight: '1.4' }],
- 'lg': ['var(--text-lg)', { lineHeight: '1.5' }],
- 'base': ['var(--text-base)', { lineHeight: '1.5' }],
- 'sm': ['var(--text-sm)', { lineHeight: '1.5' }],
- 'xs': ['var(--text-xs)', { lineHeight: '1.5' }],
- },
- // EN: Font weights from CSS variables
- // VI: Độ đậm chữ từ CSS variables
- fontWeight: {
- light: 'var(--font-light)',
- normal: 'var(--font-normal)',
- medium: 'var(--font-medium)',
- semibold: 'var(--font-semibold)',
- bold: 'var(--font-bold)',
- },
- // EN: Spacing from CSS variables (extends default Tailwind spacing)
- // VI: Khoảng cách từ CSS variables (mở rộng spacing mặc định của Tailwind)
- spacing: {
- 'sidebar': 'var(--sidebar-width)',
- 'chat-max': 'var(--chat-max-width)',
- // EN: Additional spacing utilities using CSS variables
- // VI: Các utility spacing bổ sung sử dụng CSS variables
- '0': 'var(--space-0)',
- '1': 'var(--space-1)',
- '2': 'var(--space-2)',
- '3': 'var(--space-3)',
- '4': 'var(--space-4)',
- '5': 'var(--space-5)',
- '6': 'var(--space-6)',
- '8': 'var(--space-8)',
- '10': 'var(--space-10)',
- '12': 'var(--space-12)',
- '16': 'var(--space-16)',
- '20': 'var(--space-20)',
- },
- // EN: Border radius from CSS variables
- // VI: Bo góc từ CSS variables
- borderRadius: {
- sm: 'var(--radius-sm)',
- md: 'var(--radius-md)',
- lg: 'var(--radius-lg)',
- xl: 'var(--radius-xl)',
- '2xl': 'var(--radius-2xl)',
- full: 'var(--radius-full)',
- },
- // EN: Box shadows from CSS variables
- // VI: Đổ bóng từ CSS variables
- // EN: Brand gradients / VI: Gradients thương hiệu
- backgroundImage: {
- 'brand-gradient': 'var(--brand-gradient-primary)',
- 'brand-gradient-accent': 'var(--brand-gradient-accent)',
- 'brand-gradient-vertical': 'var(--brand-gradient-vertical)',
- },
- // EN: Extended shadows (including glass shadows)
- // VI: Shadows mở rộng (bao gồm glass shadows)
- boxShadow: {
- sm: 'var(--shadow-sm)',
- md: 'var(--shadow-md)',
- lg: 'var(--shadow-lg)',
- xl: 'var(--shadow-xl)',
- glow: 'var(--shadow-glow)',
- brand: 'var(--shadow-brand)',
- 'brand-lg': 'var(--shadow-brand-lg)',
- colored: 'var(--shadow-colored)',
- // Glass shadows
- 'glass-sm': 'var(--glass-shadow-sm)',
- 'glass': 'var(--glass-shadow-md)',
- 'glass-md': 'var(--glass-shadow-md)',
- 'glass-lg': 'var(--glass-shadow-lg)',
- 'glass-inset': 'var(--glass-shadow-inset)',
- },
- // EN: Glass backdrop blur levels / VI: Các mức backdrop blur cho glass
- backdropBlur: {
- 'glass-sm': 'var(--glass-blur-sm)',
- 'glass': 'var(--glass-blur-md)',
- 'glass-md': 'var(--glass-blur-md)',
- 'glass-lg': 'var(--glass-blur-lg)',
- 'glass-xl': 'var(--glass-blur-xl)',
- },
- // EN: Animation timing functions (including motion easing)
- // VI: Hàm thời gian animation (bao gồm motion easing)
- transitionTimingFunction: {
- 'in': 'var(--ease-in)',
- 'out': 'var(--ease-out)',
- 'in-out': 'var(--ease-in-out)',
- spring: 'var(--ease-spring)',
- // Motion easing (x.ai inspired)
- smooth: 'var(--motion-ease-smooth)',
- glide: 'var(--motion-ease-glide)',
- },
- // EN: Animation durations (including motion durations)
- // VI: Thời lượng animation (bao gồm motion durations)
- transitionDuration: {
- fast: 'var(--duration-fast)',
- normal: 'var(--duration-normal)',
- slow: 'var(--duration-slow)',
- slower: 'var(--duration-slower)',
- // Motion durations (x.ai style)
- instant: 'var(--motion-duration-instant)',
- quick: 'var(--motion-duration-quick)',
- 'motion-normal': 'var(--motion-duration-normal)',
- 'motion-smooth': 'var(--motion-duration-smooth)',
- },
- // EN: Max widths for containers
- // VI: Chiều rộng tối đa cho containers
- maxWidth: {
- 'container-sm': 'var(--container-sm)',
- 'container-md': 'var(--container-md)',
- 'container-lg': 'var(--container-lg)',
- 'container-xl': 'var(--container-xl)',
- 'container-2xl': 'var(--container-2xl)',
- 'chat-max': 'var(--chat-max-width)',
- },
- // EN: Safe area utilities for mobile
- // VI: Utilities safe area cho mobile
- padding: {
- 'safe-top': 'var(--mobile-safe-area-top)',
- 'safe-bottom': 'var(--mobile-safe-area-bottom)',
- },
- margin: {
- 'safe-top': 'var(--mobile-safe-area-top)',
- 'safe-bottom': 'var(--mobile-safe-area-bottom)',
- },
- // EN: Screen breakpoints (matching CSS variables)
- // VI: Điểm ngắt màn hình (khớp với CSS variables)
- screens: {
- sm: '640px',
- md: '768px',
- lg: '1024px',
- xl: '1280px',
- '2xl': '1536px',
- },
- // EN: Line heights from CSS variables
- // VI: Chiều cao dòng từ CSS variables
- lineHeight: {
- none: 'var(--leading-none)',
- tight: 'var(--leading-tight)',
- snug: 'var(--leading-snug)',
- normal: 'var(--leading-normal)',
- relaxed: 'var(--leading-relaxed)',
- loose: 'var(--leading-loose)',
- },
- },
- },
- plugins: [
- // Custom glass utilities for Tailwind CSS 4
- function({ addUtilities }) {
- const glassUtilities = {
- '.glass-panel': {
- background: 'var(--glass-bg-default)',
- 'backdrop-filter': 'blur(var(--glass-blur-md))',
- '-webkit-backdrop-filter': 'blur(var(--glass-blur-md))',
- border: '1px solid var(--glass-border-default)',
- 'box-shadow': 'var(--glass-shadow-md), var(--glass-shadow-inset)',
- },
- '.glass-panel:hover': {
- background: 'var(--glass-bg-hover)',
- 'border-color': 'var(--glass-border-hover)',
- transition: 'all var(--motion-duration-quick) var(--motion-ease-smooth)',
- },
- };
- addUtilities(glassUtilities);
- },
- // Mobile safe area utilities
- function({ addUtilities }) {
- const mobileUtilities = {
- '.safe-area-inset-top': {
- 'padding-top': 'var(--mobile-safe-area-top)',
- },
- '.safe-area-inset-bottom': {
- 'padding-bottom': 'var(--mobile-safe-area-bottom)',
- },
- '.pb-safe-bottom': {
- 'padding-bottom': 'calc(var(--mobile-bottom-nav-height) + var(--mobile-safe-area-bottom))',
- },
- '.mobile-layout': {
- 'min-height': '100vh',
- 'min-height': '100dvh', // Dynamic viewport height for mobile
- },
- };
- addUtilities(mobileUtilities);
- },
- ],
-};
diff --git a/apps/web-client/test-results/.last-run.json b/apps/web-client/test-results/.last-run.json
deleted file mode 100644
index cbcc1fba..00000000
--- a/apps/web-client/test-results/.last-run.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "status": "passed",
- "failedTests": []
-}
\ No newline at end of file
diff --git a/apps/web-client/tsconfig.json b/apps/web-client/tsconfig.json
deleted file mode 100644
index e2164a4a..00000000
--- a/apps/web-client/tsconfig.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "extends": "@goodgo/tsconfig/nextjs.json",
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": [
- "./src/*"
- ],
- "@/features/*": [
- "./src/features/*"
- ],
- "@/shared/*": [
- "./src/features/shared/*"
- ],
- "@/ui": [
- "./src/features/shared/components/ui"
- ],
- "@/lib/*": [
- "./src/lib/*"
- ]
- },
- "isolatedModules": true,
- "noUnusedLocals": false,
- "noUnusedParameters": false
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts"
- ],
- "exclude": [
- "node_modules",
- "**/*.test.ts",
- "**/*.test.tsx",
- "**/*.spec.ts",
- "**/*.spec.tsx",
- "**/__tests__/**",
- "**/e2e/**",
- "**/test/**",
- "playwright.config.ts",
- "vitest.config.ts",
- ".storybook/**",
- "**/*.stories.ts",
- "**/*.stories.tsx"
- ]
-}
diff --git a/apps/web-client/vitest.config.ts b/apps/web-client/vitest.config.ts
deleted file mode 100644
index 554e61ef..00000000
--- a/apps/web-client/vitest.config.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { defineConfig } from 'vitest/config';
-import react from '@vitejs/plugin-react';
-import path from 'path';
-
-/**
- * EN: Vitest configuration for unit testing
- * VI: Cấu hình Vitest cho unit testing
- */
-export default defineConfig({
- plugins: [react()],
- test: {
- environment: 'jsdom',
- globals: true,
- setupFiles: ['./src/test/setup.ts'],
- include: [
- 'src/stores/__tests__/**/*.test.ts',
- 'src/services/api/__tests__/**/*.test.ts',
- ],
- exclude: ['e2e/**', 'src/**/__tests__/**/*.integration.test.tsx'],
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json', 'html'],
- exclude: [
- 'node_modules/',
- 'src/test/',
- '**/*.d.ts',
- '**/*.config.*',
- '**/mockData',
- ],
- },
- },
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- },
-});
diff --git a/deployments/local/.env b/deployments/local/.env
index 0a3dc451..70aebec3 100644
--- a/deployments/local/.env
+++ b/deployments/local/.env
@@ -1,15 +1,19 @@
-# EN: Default sanitized local environment values.
-# VI: Giá trị môi trường local mặc định đã làm sạch.
-# NOTE: Replace placeholders before running docker compose.
+# =============================================================================
+# GoodGo Platform - Local Docker Environment
+# =============================================================================
+# EN: Auto-generated for self-contained local Docker deployment.
+# VI: Tự động tạo cho deployment Docker local tự hoàn chỉnh.
+# =============================================================================
ASPNETCORE_ENVIRONMENT=Development
NODE_ENV=development
LOG_LEVEL=Information
API_VERSION=v1
-JWT_SECRET=replace-with-min-32-char-secret
-JWT_REFRESH_SECRET=replace-with-min-32-char-secret
-JWT_ID_SECRET=replace-with-min-32-char-secret
+# JWT / Auth
+JWT_SECRET=GoodGo-Local-Dev-JWT-Secret-2024-Min32Chars!!
+JWT_REFRESH_SECRET=GoodGo-Local-Dev-Refresh-Secret-2024-32Ch!!
+JWT_ID_SECRET=GoodGo-Local-Dev-ID-Secret-2024-Min32Char!!
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
JWT_ID_EXPIRES_IN=1h
@@ -18,21 +22,26 @@ JWT_AUDIENCE=goodgo-services
JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
-ENCRYPTION_KEY=replace-with-64-char-hex-key
+# Security
+ENCRYPTION_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
+# Redis (local container)
REDIS_HOST=redis
REDIS_PORT=6379
-REDIS_PASSWORD=replace-with-redis-password
+REDIS_PASSWORD=goodgo-redis-local
REDIS_DATABASE=0
-REDIS_CONNECTION_STRING=redis:6379,password=replace-with-redis-password
+REDIS_CONNECTION_STRING=redis:6379,password=goodgo-redis-local
+# MinIO (local container)
MINIO_ENDPOINT=minio:9000
-MINIO_ACCESS_KEY=replace-with-minio-access-key
-MINIO_SECRET_KEY=replace-with-minio-secret-key
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=minioadmin123
+# RabbitMQ (local container)
RABBITMQ_USERNAME=guest
-RABBITMQ_PASSWORD=replace-with-rabbitmq-password
+RABBITMQ_PASSWORD=goodgo-rabbitmq-local
+# Feature flags
FEATURE_SWAGGER_ENABLED=true
FEATURE_DETAILED_ERRORS=true
CORS_ORIGIN=http://localhost:3000,http://localhost:3001,http://localhost,http://admin.localhost
@@ -41,23 +50,24 @@ JAEGER_ENDPOINT=http://jaeger:14268/api/traces
METRICS_ENABLED=true
SEQ_URL=http://localhost:5341
-IAM_DATABASE_URL=Host=your-neon-host;Port=5432;Database=iam_service;Username=your-user;Password=your-password;SSL Mode=Require
-STORAGE_DATABASE_URL=Host=your-neon-host;Port=5432;Database=storage_service;Username=your-user;Password=your-password;SSL Mode=Require
-MEMBERSHIP_DATABASE_URL=Host=your-neon-host;Port=5432;Database=membership_service;Username=your-user;Password=your-password;SSL Mode=Require
-MERCHANT_DATABASE_URL=Host=your-neon-host;Port=5432;Database=merchant_service;Username=your-user;Password=your-password;SSL Mode=Require
-WALLET_DATABASE_URL=Host=your-neon-host;Port=5432;Database=wallet_service;Username=your-user;Password=your-password;SSL Mode=Require
-CHAT_DATABASE_URL=Host=your-neon-host;Port=5432;Database=chat_service;Username=your-user;Password=your-password;SSL Mode=Require
-SOCIAL_DATABASE_URL=Host=your-neon-host;Port=5432;Database=social_service;Username=your-user;Password=your-password;SSL Mode=Require
-MINING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mining_service;Username=your-user;Password=your-password;SSL Mode=Require
-MISSION_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mission_service;Username=your-user;Password=your-password;SSL Mode=Require
-PROMOTION_DATABASE_URL=Host=your-neon-host;Port=5432;Database=promotion_service;Username=your-user;Password=your-password;SSL Mode=Require
-CATALOG_DATABASE_URL=Host=your-neon-host;Port=5432;Database=catalog_service;Username=your-user;Password=your-password;SSL Mode=Require
-ORDER_DATABASE_URL=Host=your-neon-host;Port=5432;Database=order_service;Username=your-user;Password=your-password;SSL Mode=Require
-INVENTORY_DATABASE_URL=Host=your-neon-host;Port=5432;Database=inventory_service;Username=your-user;Password=your-password;SSL Mode=Require
-FNB_ENGINE_DATABASE_URL=Host=your-neon-host;Port=5432;Database=fnb_engine;Username=your-user;Password=your-password;SSL Mode=Require
-BOOKING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=booking_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_MANAGER_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_manager_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_ANALYTICS_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_analytics_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_SERVING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_serving_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_BILLING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_billing_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_TRACKING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_tracking_service;Username=your-user;Password=your-password;SSL Mode=Require
+# Database connection strings (local PostgreSQL container)
+IAM_DATABASE_URL=Host=postgres;Port=5432;Database=iam_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+STORAGE_DATABASE_URL=Host=postgres;Port=5432;Database=storage_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MEMBERSHIP_DATABASE_URL=Host=postgres;Port=5432;Database=membership_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MERCHANT_DATABASE_URL=Host=postgres;Port=5432;Database=merchant_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+WALLET_DATABASE_URL=Host=postgres;Port=5432;Database=wallet_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+CHAT_DATABASE_URL=Host=postgres;Port=5432;Database=chat_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+SOCIAL_DATABASE_URL=Host=postgres;Port=5432;Database=social_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MINING_DATABASE_URL=Host=postgres;Port=5432;Database=mining_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MISSION_DATABASE_URL=Host=postgres;Port=5432;Database=mission_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+PROMOTION_DATABASE_URL=Host=postgres;Port=5432;Database=promotion_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+CATALOG_DATABASE_URL=Host=postgres;Port=5432;Database=catalog_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ORDER_DATABASE_URL=Host=postgres;Port=5432;Database=order_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+INVENTORY_DATABASE_URL=Host=postgres;Port=5432;Database=inventory_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+FNB_ENGINE_DATABASE_URL=Host=postgres;Port=5432;Database=fnb_engine;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+BOOKING_DATABASE_URL=Host=postgres;Port=5432;Database=booking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_MANAGER_DATABASE_URL=Host=postgres;Port=5432;Database=ads_manager_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_ANALYTICS_DATABASE_URL=Host=postgres;Port=5432;Database=ads_analytics_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_SERVING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_serving_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_BILLING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_billing_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_TRACKING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_tracking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
diff --git a/deployments/local/.env.local b/deployments/local/.env.local
index 1d62322b..70aebec3 100644
--- a/deployments/local/.env.local
+++ b/deployments/local/.env.local
@@ -1,14 +1,19 @@
-# EN: Local override file template (sanitized). Keep values aligned with .env.
-# VI: Template local override (đã làm sạch). Giữ giá trị đồng bộ với .env.
+# =============================================================================
+# GoodGo Platform - Local Docker Environment
+# =============================================================================
+# EN: Auto-generated for self-contained local Docker deployment.
+# VI: Tự động tạo cho deployment Docker local tự hoàn chỉnh.
+# =============================================================================
ASPNETCORE_ENVIRONMENT=Development
NODE_ENV=development
LOG_LEVEL=Information
API_VERSION=v1
-JWT_SECRET=replace-with-min-32-char-secret
-JWT_REFRESH_SECRET=replace-with-min-32-char-secret
-JWT_ID_SECRET=replace-with-min-32-char-secret
+# JWT / Auth
+JWT_SECRET=GoodGo-Local-Dev-JWT-Secret-2024-Min32Chars!!
+JWT_REFRESH_SECRET=GoodGo-Local-Dev-Refresh-Secret-2024-32Ch!!
+JWT_ID_SECRET=GoodGo-Local-Dev-ID-Secret-2024-Min32Char!!
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
JWT_ID_EXPIRES_IN=1h
@@ -17,21 +22,26 @@ JWT_AUDIENCE=goodgo-services
JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
-ENCRYPTION_KEY=replace-with-64-char-hex-key
+# Security
+ENCRYPTION_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
+# Redis (local container)
REDIS_HOST=redis
REDIS_PORT=6379
-REDIS_PASSWORD=replace-with-redis-password
+REDIS_PASSWORD=goodgo-redis-local
REDIS_DATABASE=0
-REDIS_CONNECTION_STRING=redis:6379,password=replace-with-redis-password
+REDIS_CONNECTION_STRING=redis:6379,password=goodgo-redis-local
+# MinIO (local container)
MINIO_ENDPOINT=minio:9000
-MINIO_ACCESS_KEY=replace-with-minio-access-key
-MINIO_SECRET_KEY=replace-with-minio-secret-key
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=minioadmin123
+# RabbitMQ (local container)
RABBITMQ_USERNAME=guest
-RABBITMQ_PASSWORD=replace-with-rabbitmq-password
+RABBITMQ_PASSWORD=goodgo-rabbitmq-local
+# Feature flags
FEATURE_SWAGGER_ENABLED=true
FEATURE_DETAILED_ERRORS=true
CORS_ORIGIN=http://localhost:3000,http://localhost:3001,http://localhost,http://admin.localhost
@@ -40,23 +50,24 @@ JAEGER_ENDPOINT=http://jaeger:14268/api/traces
METRICS_ENABLED=true
SEQ_URL=http://localhost:5341
-IAM_DATABASE_URL=Host=your-neon-host;Port=5432;Database=iam_service;Username=your-user;Password=your-password;SSL Mode=Require
-STORAGE_DATABASE_URL=Host=your-neon-host;Port=5432;Database=storage_service;Username=your-user;Password=your-password;SSL Mode=Require
-MEMBERSHIP_DATABASE_URL=Host=your-neon-host;Port=5432;Database=membership_service;Username=your-user;Password=your-password;SSL Mode=Require
-MERCHANT_DATABASE_URL=Host=your-neon-host;Port=5432;Database=merchant_service;Username=your-user;Password=your-password;SSL Mode=Require
-WALLET_DATABASE_URL=Host=your-neon-host;Port=5432;Database=wallet_service;Username=your-user;Password=your-password;SSL Mode=Require
-CHAT_DATABASE_URL=Host=your-neon-host;Port=5432;Database=chat_service;Username=your-user;Password=your-password;SSL Mode=Require
-SOCIAL_DATABASE_URL=Host=your-neon-host;Port=5432;Database=social_service;Username=your-user;Password=your-password;SSL Mode=Require
-MINING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mining_service;Username=your-user;Password=your-password;SSL Mode=Require
-MISSION_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mission_service;Username=your-user;Password=your-password;SSL Mode=Require
-PROMOTION_DATABASE_URL=Host=your-neon-host;Port=5432;Database=promotion_service;Username=your-user;Password=your-password;SSL Mode=Require
-CATALOG_DATABASE_URL=Host=your-neon-host;Port=5432;Database=catalog_service;Username=your-user;Password=your-password;SSL Mode=Require
-ORDER_DATABASE_URL=Host=your-neon-host;Port=5432;Database=order_service;Username=your-user;Password=your-password;SSL Mode=Require
-INVENTORY_DATABASE_URL=Host=your-neon-host;Port=5432;Database=inventory_service;Username=your-user;Password=your-password;SSL Mode=Require
-FNB_ENGINE_DATABASE_URL=Host=your-neon-host;Port=5432;Database=fnb_engine;Username=your-user;Password=your-password;SSL Mode=Require
-BOOKING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=booking_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_MANAGER_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_manager_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_ANALYTICS_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_analytics_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_SERVING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_serving_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_BILLING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_billing_service;Username=your-user;Password=your-password;SSL Mode=Require
-ADS_TRACKING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_tracking_service;Username=your-user;Password=your-password;SSL Mode=Require
+# Database connection strings (local PostgreSQL container)
+IAM_DATABASE_URL=Host=postgres;Port=5432;Database=iam_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+STORAGE_DATABASE_URL=Host=postgres;Port=5432;Database=storage_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MEMBERSHIP_DATABASE_URL=Host=postgres;Port=5432;Database=membership_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MERCHANT_DATABASE_URL=Host=postgres;Port=5432;Database=merchant_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+WALLET_DATABASE_URL=Host=postgres;Port=5432;Database=wallet_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+CHAT_DATABASE_URL=Host=postgres;Port=5432;Database=chat_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+SOCIAL_DATABASE_URL=Host=postgres;Port=5432;Database=social_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MINING_DATABASE_URL=Host=postgres;Port=5432;Database=mining_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+MISSION_DATABASE_URL=Host=postgres;Port=5432;Database=mission_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+PROMOTION_DATABASE_URL=Host=postgres;Port=5432;Database=promotion_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+CATALOG_DATABASE_URL=Host=postgres;Port=5432;Database=catalog_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ORDER_DATABASE_URL=Host=postgres;Port=5432;Database=order_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+INVENTORY_DATABASE_URL=Host=postgres;Port=5432;Database=inventory_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+FNB_ENGINE_DATABASE_URL=Host=postgres;Port=5432;Database=fnb_engine;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+BOOKING_DATABASE_URL=Host=postgres;Port=5432;Database=booking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_MANAGER_DATABASE_URL=Host=postgres;Port=5432;Database=ads_manager_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_ANALYTICS_DATABASE_URL=Host=postgres;Port=5432;Database=ads_analytics_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_SERVING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_serving_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_BILLING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_billing_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
+ADS_TRACKING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_tracking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable
diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml
index 31ab6600..ca9925e9 100644
--- a/deployments/local/docker-compose.yml
+++ b/deployments/local/docker-compose.yml
@@ -1,4 +1,4 @@
-version: '3.8'
+
# =============================================================================
# GoodGo Platform - Local Development Environment
@@ -27,6 +27,92 @@ services:
# SHARED INFRASTRUCTURE
# ===========================================================================
+ # PostgreSQL 16 - Shared Database Server
+ postgres:
+ image: postgres:15-alpine
+ container_name: postgres-local
+ environment:
+ - POSTGRES_USER=goodgo
+ - POSTGRES_PASSWORD=goodgo-local-2024
+ - POSTGRES_DB=postgres
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U goodgo"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+
+ # Redis 7 - Cache & SignalR Backplane
+ redis:
+ image: redis:7-alpine
+ container_name: redis-local
+ command: redis-server --requirepass goodgo-redis-local
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "redis-cli", "-a", "goodgo-redis-local", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ # MinIO - Object Storage (S3-compatible)
+ minio:
+ image: minio/minio:latest
+ container_name: minio-local
+ command: server /data --console-address ":9001"
+ environment:
+ - MINIO_ROOT_USER=minioadmin
+ - MINIO_ROOT_PASSWORD=minioadmin123
+ ports:
+ - "9000:9000" # API
+ - "9001:9001" # Console
+ volumes:
+ - minio_data:/data
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+
+ # RabbitMQ 3 - Message Broker (for Ads services)
+ rabbitmq:
+ image: rabbitmq:3-management-alpine
+ container_name: rabbitmq-local
+ environment:
+ - RABBITMQ_DEFAULT_USER=guest
+ - RABBITMQ_DEFAULT_PASS=goodgo-rabbitmq-local
+ ports:
+ - "5672:5672" # AMQP
+ - "15672:15672" # Management UI
+ volumes:
+ - rabbitmq_data:/var/lib/rabbitmq
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
+ interval: 15s
+ timeout: 10s
+ retries: 5
+ start_period: 30s
+
# Traefik - API Gateway and Reverse Proxy
traefik:
image: traefik:v3.3
@@ -47,7 +133,7 @@ services:
volumes:
# EN: Use actual Docker Desktop socket path (not symlink)
# VI: Sử dụng đường dẫn socket thực của Docker Desktop (không phải symlink)
- - ${HOME}/.docker/run/docker.sock:/var/run/docker.sock:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
- ../../infra/traefik:/etc/traefik:ro
networks:
- microservices-network
@@ -72,23 +158,15 @@ services:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:8080
- # EN: Database - Neon PostgreSQL
- # VI: Cơ sở dữ liệu - Neon PostgreSQL
- ConnectionStrings__DefaultConnection=${STORAGE_DATABASE_URL}
- # EN: Storage - External MinIO
- # VI: Storage - MinIO bên ngoài
- Storage__Provider=minio
- Storage__DefaultBucket=goodgo
- Storage__MinIO__Endpoint=${MINIO_ENDPOINT}
- Storage__MinIO__AccessKey=${MINIO_ACCESS_KEY}
- Storage__MinIO__SecretKey=${MINIO_SECRET_KEY}
- Storage__MinIO__UseSSL=false
- # EN: IAM Service Communication
- # VI: Giao tiếp IAM Service
- IamService__BaseUrl=http://iam-service-net:8080
- IamService__ServiceName=storage-service
- # EN: Redis Cache
- # VI: Cache Redis
- Redis__Host=${REDIS_HOST}
- Redis__Port=${REDIS_PORT}
- Redis__Password=${REDIS_PASSWORD}
@@ -103,7 +181,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -145,7 +223,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -191,7 +269,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -242,17 +320,21 @@ services:
ports:
- "5001:8080"
depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
traefik:
condition: service_started
networks:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
- start_period: 15s
+ start_period: 60s
labels:
- "traefik.enable=true"
- "traefik.http.routers.iam-service-net.rule=PathPrefix(`/api/v1/iam`) || PathPrefix(`/api/v1/auth`) || PathPrefix(`/api/v1/users`) || PathPrefix(`/api/v1/roles`)"
@@ -294,7 +376,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -347,7 +429,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -407,7 +489,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -458,7 +540,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -516,7 +598,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -567,7 +649,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -619,7 +701,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -672,7 +754,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -720,7 +802,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -771,7 +853,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -832,7 +914,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -884,7 +966,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -935,7 +1017,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -992,7 +1074,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -1050,7 +1132,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -1114,7 +1196,7 @@ services:
- microservices-network
restart: unless-stopped
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -1180,10 +1262,61 @@ services:
# - microservices-network
# restart: unless-stopped
+ # ===========================================================================
+ # FRONTEND APPS
+ # ===========================================================================
+
+ # Web Client TPOS .NET - Blazor WebAssembly Hosted
+ web-client-tpos-net:
+ build:
+ context: ../../apps/web-client-tpos-net
+ dockerfile: Dockerfile
+ image: goodgo/web-client-tpos-net:latest
+ container_name: web-client-tpos-net-local
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ASPNETCORE_URLS=http://+:8080
+ # EN: API Gateway URL for backend communication
+ # VI: URL API Gateway để giao tiếp với backend
+ - ApiSettings__GatewayUrl=http://traefik:80
+ # EN: IAM Service Communication
+ # VI: Giao tiếp IAM Service
+ - IamService__BaseUrl=http://iam-service-net:8080
+ ports:
+ - "3001:8080"
+ depends_on:
+ iam-service-net:
+ condition: service_healthy
+ traefik:
+ condition: service_started
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.web-client.rule=Host(`localhost`)"
+ - "traefik.http.routers.web-client.entrypoints=web"
+ - "traefik.http.routers.web-client.priority=1"
+ - "traefik.http.services.web-client.loadbalancer.server.port=8080"
+
# =============================================================================
# VOLUMES
# =============================================================================
-volumes: {}
+volumes:
+ postgres_data:
+ driver: local
+ redis_data:
+ driver: local
+ minio_data:
+ driver: local
+ rabbitmq_data:
+ driver: local
# =============================================================================
# NETWORKS
# =============================================================================
diff --git a/deployments/local/init-databases.sh b/deployments/local/init-databases.sh
new file mode 100755
index 00000000..02fffe1a
--- /dev/null
+++ b/deployments/local/init-databases.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# =============================================================================
+# GoodGo Platform - PostgreSQL Database Initialization Script
+# =============================================================================
+# EN: Creates all required databases for each microservice on first startup.
+# VI: Tạo tất cả databases cần thiết cho từng microservice khi khởi động lần đầu.
+# =============================================================================
+
+set -e
+
+DATABASES=(
+ "iam_service"
+ "storage_service"
+ "membership_service"
+ "merchant_service"
+ "wallet_service"
+ "chat_service"
+ "social_service"
+ "mining_service"
+ "mission_service"
+ "promotion_service"
+ "catalog_service"
+ "order_service"
+ "inventory_service"
+ "fnb_engine"
+ "booking_service"
+ "ads_manager_service"
+ "ads_analytics_service"
+ "ads_serving_service"
+ "ads_billing_service"
+ "ads_tracking_service"
+)
+
+echo "=== GoodGo: Creating databases ==="
+
+for DB_NAME in "${DATABASES[@]}"; do
+ echo "Creating database: $DB_NAME"
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
+ SELECT 'CREATE DATABASE $DB_NAME'
+ WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec
+EOSQL
+done
+
+echo "=== GoodGo: All databases created successfully ==="
diff --git a/services/ads-analytics-service-net/Dockerfile b/services/ads-analytics-service-net/Dockerfile
index 673e2bd8..c5692cc4 100644
--- a/services/ads-analytics-service-net/Dockerfile
+++ b/services/ads-analytics-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/ads-billing-service-net/Dockerfile b/services/ads-billing-service-net/Dockerfile
index f3f096a9..1e45725a 100644
--- a/services/ads-billing-service-net/Dockerfile
+++ b/services/ads-billing-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/ads-manager-service-net/Dockerfile b/services/ads-manager-service-net/Dockerfile
index b228ddaa..acc1348a 100644
--- a/services/ads-manager-service-net/Dockerfile
+++ b/services/ads-manager-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/ads-serving-service-net/Dockerfile b/services/ads-serving-service-net/Dockerfile
index 56cddee6..5043ca7b 100644
--- a/services/ads-serving-service-net/Dockerfile
+++ b/services/ads-serving-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
index 92308f7c..4dc11670 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
@@ -50,9 +50,9 @@ public class GetAuctionsQueryHandler : IRequestHandler(a, "_auctionTime"),
BidsJson = EF.Property(a, "_bidsJson"),
- WinningAdId = a.Result != null ? a.Result.WinningAdId : null,
- FinalPrice = a.Result != null ? a.Result.FinalPrice : null,
- WinningeCPM = a.Result != null ? a.Result.WinningeCPM : null
+ WinningAdId = a.Result != null ? (Guid?)a.Result.WinningAdId : null,
+ FinalPrice = a.Result != null ? (decimal?)a.Result.FinalPrice : null,
+ WinningeCPM = a.Result != null ? (decimal?)a.Result.WinningeCPM : null
})
.ToListAsync(cancellationToken);
diff --git a/services/ads-tracking-service-net/Dockerfile b/services/ads-tracking-service-net/Dockerfile
index cb8ba09b..b9acae91 100644
--- a/services/ads-tracking-service-net/Dockerfile
+++ b/services/ads-tracking-service-net/Dockerfile
@@ -41,7 +41,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/booking-service-net/Dockerfile b/services/booking-service-net/Dockerfile
index 6e29cc0b..499ca939 100644
--- a/services/booking-service-net/Dockerfile
+++ b/services/booking-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/catalog-service-net/Dockerfile b/services/catalog-service-net/Dockerfile
index 00fa8d32..3af3035f 100644
--- a/services/catalog-service-net/Dockerfile
+++ b/services/catalog-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/chat-service-net/Dockerfile b/services/chat-service-net/Dockerfile
index 03f13b9a..815d0542 100644
--- a/services/chat-service-net/Dockerfile
+++ b/services/chat-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/fnb-engine-net/Dockerfile b/services/fnb-engine-net/Dockerfile
index b3bfa54b..0e48ccbb 100644
--- a/services/fnb-engine-net/Dockerfile
+++ b/services/fnb-engine-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs
index 292ecc32..d7278433 100644
--- a/services/iam-service-net/src/IamService.API/Program.cs
+++ b/services/iam-service-net/src/IamService.API/Program.cs
@@ -1,4 +1,5 @@
using Asp.Versioning;
+using Microsoft.EntityFrameworkCore;
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using IamService.API.Application.Services;
@@ -255,11 +256,21 @@ var app = builder.Build();
var logger = app.Services.GetRequiredService>();
logger.LogInformation("Starting IAM Service API / Khởi động IAM Service API");
+// EN: Auto-apply EF Core migrations on startup (Development only)
+// VI: Tự động áp dụng EF Core migrations khi khởi động (chỉ Development)
+if (app.Environment.IsDevelopment())
+{
+ using var scope = app.Services.CreateScope();
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ logger.LogInformation("Applying EF Core migrations...");
+ await dbContext.Database.MigrateAsync();
+ logger.LogInformation("EF Core migrations applied successfully.");
+}
+
// EN: Seed system roles on startup
// VI: Seed system roles khi khởi động
await IamService.Infrastructure.Data.DataSeeder.SeedRolesAsync(app.Services);
-
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
app.UseSerilogRequestLogging();
app.UseProblemDetails();
diff --git a/services/inventory-service-net/Dockerfile b/services/inventory-service-net/Dockerfile
index ee1e8f30..7e61ee30 100644
--- a/services/inventory-service-net/Dockerfile
+++ b/services/inventory-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/mining-service-net/Dockerfile b/services/mining-service-net/Dockerfile
index f928911d..0e7bf7ba 100644
--- a/services/mining-service-net/Dockerfile
+++ b/services/mining-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/mission-service-net/Dockerfile b/services/mission-service-net/Dockerfile
index 8ed55dc4..496d82ec 100644
--- a/services/mission-service-net/Dockerfile
+++ b/services/mission-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/order-service-net/Dockerfile b/services/order-service-net/Dockerfile
index 0737d3b3..c96211f0 100644
--- a/services/order-service-net/Dockerfile
+++ b/services/order-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/promotion-service-net/Dockerfile b/services/promotion-service-net/Dockerfile
index 5a0496d6..930e1f76 100644
--- a/services/promotion-service-net/Dockerfile
+++ b/services/promotion-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/social-service-net/Dockerfile b/services/social-service-net/Dockerfile
index 277f9f2e..de870904 100644
--- a/services/social-service-net/Dockerfile
+++ b/services/social-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/services/storage-service-net/src/StorageService.Infrastructure/Storage/AliyunOssStorageProvider.cs b/services/storage-service-net/src/StorageService.Infrastructure/Storage/AliyunOssStorageProvider.cs
index 5e7a5171..fc700375 100644
--- a/services/storage-service-net/src/StorageService.Infrastructure/Storage/AliyunOssStorageProvider.cs
+++ b/services/storage-service-net/src/StorageService.Infrastructure/Storage/AliyunOssStorageProvider.cs
@@ -177,10 +177,10 @@ public class AliyunOssStorageProvider : IStorageProvider
{
EnsureBucketExistsAsync(bucketName, cancellationToken).Wait(cancellationToken);
- var request = new InitiateMultipartUploadRequest(bucketName, objectKey)
- {
- ContentType = contentType
- };
+ var request = new InitiateMultipartUploadRequest(bucketName, objectKey);
+ // EN: Set ContentType via ObjectMetadata (SDK doesn't expose it as direct property)
+ // VI: Đặt ContentType qua ObjectMetadata (SDK không expose nó như property trực tiếp)
+ request.ObjectMetadata = new ObjectMetadata { ContentType = contentType };
var response = _ossClient.InitiateMultipartUpload(request);
_logger.LogInformation(
@@ -261,7 +261,12 @@ public class AliyunOssStorageProvider : IStorageProvider
.Select(part => new OssPartETag(part.PartNumber, part.ETag))
.ToList();
- request.PartETags.AddRange(orderedParts);
+ // EN: IList doesn't have AddRange — add items individually
+ // VI: IList không có AddRange — thêm từng item
+ foreach (var part in orderedParts)
+ {
+ request.PartETags.Add(part);
+ }
_ossClient.CompleteMultipartUpload(request);
_logger.LogInformation(
diff --git a/services/wallet-service-net/Dockerfile b/services/wallet-service-net/Dockerfile
index 4e68c9a1..b7ad92ed 100644
--- a/services/wallet-service-net/Dockerfile
+++ b/services/wallet-service-net/Dockerfile
@@ -32,7 +32,8 @@ WORKDIR /app
# EN: Create non-root user for security
# VI: Tạo user non-root cho bảo mật
-RUN groupadd -g 1001 dotnetuser && \
+RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* && \
+ groupadd -g 1001 dotnetuser && \
useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
# EN: Copy published application
diff --git a/turbo.json b/turbo.json
index 4c42f8ba..f1feced0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,19 +1,29 @@
{
"$schema": "https://turbo.build/schema.json",
- "pipeline": {
+ "tasks": {
"build": {
- "dependsOn": ["^build"],
- "outputs": ["dist/**", ".next/**", "build/**"]
+ "dependsOn": [
+ "^build"
+ ],
+ "outputs": [
+ "dist/**",
+ ".next/**",
+ "build/**"
+ ]
},
"test": {
- "dependsOn": ["build"],
+ "dependsOn": [
+ "build"
+ ],
"outputs": []
},
"lint": {
"outputs": []
},
"typecheck": {
- "dependsOn": ["^build"],
+ "dependsOn": [
+ "^build"
+ ],
"outputs": []
},
"dev": {
@@ -24,4 +34,4 @@
"cache": false
}
}
-}
+}
\ No newline at end of file