From b831058a8fcd4ff9633820c0760d2096618fcceb Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 4 Jan 2026 15:40:12 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20Th=C3=AAm=20header=20=C4=91i=E1=BB=81u?= =?UTF-8?q?=20h=C6=B0=E1=BB=9Bng=20responsive,=20b=E1=BB=99=20chuy?= =?UTF-8?q?=E1=BB=83n=20=C4=91=E1=BB=95i=20ng=C3=B4n=20ng=E1=BB=AF,=20th?= =?UTF-8?q?=C3=A0nh=20ph=E1=BA=A7n=20UI=20chuy=E1=BB=83n=20=C4=91=E1=BB=95?= =?UTF-8?q?i=20ch=E1=BB=A7=20=C4=91=E1=BB=81=20v=C3=A0=20ph=C3=ADm=20t?= =?UTF-8?q?=E1=BA=AFt=20chuy=E1=BB=83n=20=C4=91=E1=BB=95i=20ch=E1=BB=A7=20?= =?UTF-8?q?=C4=91=E1=BB=81.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-client/src/app/globals.css | 62 +++++++ .../components/layout/navigation-header.tsx | 161 ++++++++++++++++ .../src/components/ui/language-switcher.tsx | 172 ++++++++++++++++++ .../components/ui/theme-toggle-enhanced.tsx | 106 +++++++++++ .../web-client/src/contexts/theme-context.tsx | 24 ++- 5 files changed, 521 insertions(+), 4 deletions(-) create mode 100644 apps/web-client/src/components/layout/navigation-header.tsx create mode 100644 apps/web-client/src/components/ui/language-switcher.tsx create mode 100644 apps/web-client/src/components/ui/theme-toggle-enhanced.tsx diff --git a/apps/web-client/src/app/globals.css b/apps/web-client/src/app/globals.css index 3e05ecdf..cfe9ade9 100644 --- a/apps/web-client/src/app/globals.css +++ b/apps/web-client/src/app/globals.css @@ -182,4 +182,66 @@ .animate-shimmer { animation: shimmer 2s infinite; } + + /* ============================================ + EN: Responsive Layout Utilities + VI: Utilities cho responsive layout + ============================================ */ + + /* Mobile-first container */ + .container-responsive { + width: 100%; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + + @media (min-width: 640px) { + .container-responsive { + max-width: 640px; + } + } + + @media (min-width: 768px) { + .container-responsive { + max-width: 768px; + padding-left: 1.5rem; + padding-right: 1.5rem; + } + } + + @media (min-width: 1024px) { + .container-responsive { + max-width: 1024px; + padding-left: 2rem; + padding-right: 2rem; + } + } + + @media (min-width: 1280px) { + .container-responsive { + max-width: 1280px; + } + } + + /* Mobile-optimized text sizes */ + .text-responsive-hero { + font-size: 2.5rem; + /* 40px mobile */ + line-height: 1.1; + } + + @media (min-width: 768px) { + .text-responsive-hero { + font-size: 3.75rem; + /* 60px desktop */ + } + } + + /* Touch-friendly buttons on mobile */ + .btn-touch { + min-height: 44px; + min-width: 44px; + } } \ No newline at end of file diff --git a/apps/web-client/src/components/layout/navigation-header.tsx b/apps/web-client/src/components/layout/navigation-header.tsx new file mode 100644 index 00000000..3d31c6cf --- /dev/null +++ b/apps/web-client/src/components/layout/navigation-header.tsx @@ -0,0 +1,161 @@ +'use client'; + +import React, { useState } from 'react'; +import { Menu, X } from 'lucide-react'; +import { BrandLogo, BrandLogoLink } from '../ui/brand-logo'; +import { ThemeToggle } from '../ui/theme-toggle-enhanced'; +import { LanguageSwitcher } from '../ui/language-switcher'; +import { Button } from '../ui/button'; +import { cn } from '@/lib/utils'; + +/** + * EN: Navigation Header props + * VI: Props cho Navigation Header + */ +export interface NavigationHeaderProps { + /** Show CTA button / Hiển thị button CTA */ + showCTA?: boolean; + /** CTA button text / Text button CTA */ + ctaText?: string; + /** CTA button onClick / onClick button CTA */ + onCTAClick?: () => void; + /** Additional CSS classes / CSS classes bổ sung */ + className?: string; +} + +/** + * EN: Navigation Header - Responsive header with brand logo, theme toggle, and language switcher + * VI: Navigation Header - Header responsive với logo, theme toggle và language switcher + * + * Features: + * - Sticky header with glassmorphism + * - Responsive design (mobile menu) + * - Brand logo integration + * - Theme toggle + * - Language switcher + * - CTA button + * + * @example + * ```tsx + * router.push('/auth/login')} + * /> + * ``` + */ +export function NavigationHeader({ + showCTA = true, + ctaText = 'Get Started', + onCTAClick, + className, +}: NavigationHeaderProps) { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + return ( +
+
+ {/* EN: Main header content / VI: Nội dung header chính */} +
+ {/* EN: Logo / VI: Logo */} + + + {/* EN: Desktop Navigation / VI: Navigation desktop */} + + + {/* EN: Mobile Menu Button / VI: Button menu mobile */} + +
+ + {/* EN: Mobile Menu / VI: Menu mobile */} + {mobileMenuOpen && ( +
+
+ + +
+ {showCTA && ( + + )} +
+ )} +
+
+ ); +} + +/** + * EN: Minimal Navigation Header - Simplified version without CTA + * VI: Navigation Header tối giản - Phiên bản đơn giản không có CTA + * + * @example + * ```tsx + * + * ``` + */ +export function MinimalNavigationHeader({ className }: { className?: string }) { + return ( +
+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web-client/src/components/ui/language-switcher.tsx b/apps/web-client/src/components/ui/language-switcher.tsx new file mode 100644 index 00000000..0a816e95 --- /dev/null +++ b/apps/web-client/src/components/ui/language-switcher.tsx @@ -0,0 +1,172 @@ +'use client'; + +import React from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { Button } from './button'; +import { DropdownMenu } from './dropdown-menu'; +import { cn } from '@/lib/utils'; + +/** + * EN: Language configuration + * VI: Cấu hình ngôn ngữ + */ +const languages = [ + { code: 'en', label: 'English', flag: '🇺🇸', nativeName: 'English' }, + { code: 'vi', label: 'Tiếng Việt', flag: '🇻🇳', nativeName: 'Tiếng Việt' }, +] as const; + +export type LanguageCode = typeof languages[number]['code']; + +/** + * EN: Language Switcher - Multi-language support with flag icons + * VI: Bộ chuyển ngôn ngữ - Hỗ trợ đa ngôn ngữ với flag icons + * + * Features: + * - Flag emoji icons for visual identification + * - Dropdown menu for language selection + * - Persists locale to localStorage + * - Updates URL locale parameter + * - Smooth language switching + * + * @example + * ```tsx + * + * ``` + */ +export function LanguageSwitcher() { + const router = useRouter(); + const pathname = usePathname(); + + // EN: Extract current locale from pathname + // VI: Trích xuất locale hiện tại từ pathname + const currentLocale = pathname.startsWith('/vi') ? 'vi' : 'en'; + + 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) => { + // EN: Save to localStorage + // VI: Lưu vào localStorage + if (typeof window !== 'undefined') { + localStorage.setItem('preferredLocale', newLocale); + } + + // EN: Update URL + // VI: Cập nhật URL + let newPathname = pathname; + + // Remove existing locale prefix + if (pathname.startsWith('/en') || pathname.startsWith('/vi')) { + newPathname = pathname.substring(3) || '/'; + } + + // Add new locale prefix + if (newLocale !== 'en') { + newPathname = `/${newLocale}${newPathname}`; + } + + router.push(newPathname); + }; + + return ( + + + + + + + {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.flag} + +
+ {language.nativeName} + {language.label} +
+ {isActive && ( + + )} +
+ ); + })} +
+
+ ); +} + +/** + * EN: Compact Language Switcher - Shows only flags + * VI: Language Switcher nhỏ gọn - Chỉ hiển thị cờ + * + * @example + * ```tsx + * + * ``` + */ +export function LanguageSwitcherCompact() { + const router = useRouter(); + const pathname = usePathname(); + const currentLocale = pathname.startsWith('/vi') ? 'vi' : 'en'; + + const toggleLanguage = () => { + const newLocale = currentLocale === 'en' ? 'vi' : 'en'; + + if (typeof window !== 'undefined') { + localStorage.setItem('preferredLocale', newLocale); + } + + let newPathname = pathname; + if (pathname.startsWith('/en') || pathname.startsWith('/vi')) { + newPathname = pathname.substring(3) || '/'; + } + if (newLocale !== 'en') { + newPathname = `/${newLocale}${newPathname}`; + } + + router.push(newPathname); + }; + + const currentLanguage = languages.find(lang => lang.code === currentLocale) || languages[0]; + + return ( + + ); +} diff --git a/apps/web-client/src/components/ui/theme-toggle-enhanced.tsx b/apps/web-client/src/components/ui/theme-toggle-enhanced.tsx new file mode 100644 index 00000000..0aac7e8b --- /dev/null +++ b/apps/web-client/src/components/ui/theme-toggle-enhanced.tsx @@ -0,0 +1,106 @@ +'use client'; + +import React from 'react'; +import { useTheme } from '@/contexts/theme-context'; +import { Sun, Moon, Monitor } from 'lucide-react'; +import { Button } from './button'; +import { DropdownMenu } from './dropdown-menu'; +import { cn } from '@/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/contexts/theme-context.tsx b/apps/web-client/src/contexts/theme-context.tsx index 83aa595d..884d8b3f 100644 --- a/apps/web-client/src/contexts/theme-context.tsx +++ b/apps/web-client/src/contexts/theme-context.tsx @@ -44,7 +44,7 @@ const getSystemTheme = (): 'light' | 'dark' => { */ const applyTheme = (theme: 'light' | 'dark') => { if (typeof window === 'undefined') return; - + const root = document.documentElement; root.classList.remove('light', 'dark'); root.classList.add(theme); @@ -61,7 +61,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setThemeState] = useState('system'); const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => { if (typeof window === 'undefined') return 'dark'; - + // EN: Load from localStorage or default to system // VI: Load từ localStorage hoặc mặc định là system const stored = localStorage.getItem('theme') as ThemeMode | null; @@ -75,7 +75,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { // 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); @@ -119,7 +119,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { */ const setTheme = useCallback((newTheme: ThemeMode) => { setThemeState(newTheme); - + if (typeof window !== 'undefined') { localStorage.setItem('theme', newTheme); } @@ -139,6 +139,22 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { 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(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.ctrlKey && e.shiftKey && e.key === 'T') { + e.preventDefault(); + toggleTheme(); + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + } + }, [toggleTheme]); + const value: ThemeContextValue = { theme, resolvedTheme,