feat: Thêm thành phần footer, loại bỏ biểu tượng cờ khỏi bộ chuyển đổi ngôn ngữ, và cải thiện an toàn SSR cho ngữ cảnh chủ đề.

This commit is contained in:
Ho Ngoc Hai
2026-01-04 16:00:36 +07:00
parent b831058a8f
commit 5db698be8b
7 changed files with 385 additions and 119 deletions

View File

@@ -5,6 +5,9 @@ import { useEffect, useState } from 'react';
import { useTranslation } from '@/hooks/use-translation';
import { BrandLogo } from '@/components/ui/brand-logo';
import { Button } from '@/components/ui/button';
import { Footer } from '@/components/layout/footer';
import { NavigationHeader } from '@/components/layout/navigation-header';
import { Zap, Building, Code } from 'lucide-react';
/**
* EN: Home page component - main application entry point with brand elements
@@ -56,101 +59,104 @@ export default function Home() {
}
return (
// EN: Main content area with brand gradient background
// VI: Khu vực nội dung chính với background gradient thương hiệu
<main className="relative min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary overflow-hidden">
{/* EN: Brand gradient overlay / VI: Overlay gradient thương hiệu */}
<div className="absolute inset-0 bg-brand-gradient opacity-5 -z-10" />
<>
<NavigationHeader />
{/* EN: Main content area with brand gradient background / VI: Khu vực nội dung chính với background gradient thương hiệu */}
<main className="relative min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary overflow-hidden">
{/* EN: Brand gradient overlay / VI: Overlay gradient thương hiệu */}
<div className="absolute inset-0 bg-brand-gradient opacity-5 -z-10" />
{/* EN: Decorative brand dots / VI: Chấm trang trí thương hiệu */}
<div className="absolute top-20 left-10 w-3 h-3 rounded-full bg-brand-primary opacity-20 animate-pulse" />
<div className="absolute top-40 right-20 w-4 h-4 rounded-full bg-brand-secondary opacity-20 animate-pulse" style={{ animationDelay: '1s' }} />
<div className="absolute bottom-32 left-1/4 w-2 h-2 rounded-full bg-brand-accent opacity-20 animate-pulse" style={{ animationDelay: '2s' }} />
{/* EN: Decorative brand dots / VI: Chấm trang trí thương hiệu */}
<div className="absolute top-20 left-10 w-3 h-3 rounded-full bg-brand-primary opacity-20 animate-pulse" />
<div className="absolute top-40 right-20 w-4 h-4 rounded-full bg-brand-secondary opacity-20 animate-pulse" style={{ animationDelay: '1s' }} />
<div className="absolute bottom-32 left-1/4 w-2 h-2 rounded-full bg-brand-accent opacity-20 animate-pulse" style={{ animationDelay: '2s' }} />
<div className="max-w-container-lg w-full space-y-12 animate-fadeIn z-10">
{/* EN: Brand Logo / VI: Logo thương hiệu */}
<div className="flex justify-center mb-8">
<BrandLogo variant="full" size="xl" priority />
</div>
<div className="max-w-container-lg w-full space-y-12 animate-fadeIn z-10">
{/* EN: Brand Logo / VI: Logo thương hiệu */}
<div className="flex justify-center mb-8">
<BrandLogo variant="full" size="xl" priority />
</div>
{/* EN: Hero Title / VI: Tiêu đề Hero */}
<h1 className="text-6xl font-display font-bold tracking-tight mb-4 bg-clip-text text-transparent bg-brand-gradient-vertical leading-tight">
{t('home.title')}
</h1>
{/* EN: Hero Title / VI: Tiêu đề Hero */}
<h1 className="text-6xl font-display font-bold tracking-tight mb-4 bg-clip-text text-transparent bg-brand-gradient-vertical leading-tight">
{t('home.title')}
</h1>
{/* EN: Subtitle/Description / VI: Phụ đề/Mô tả */}
<p className="text-xl text-text-secondary max-w-2xl mx-auto leading-relaxed">
{t('home.description')}
</p>
{/* EN: Subtitle/Description / VI: Phụ đề/Mô tả */}
<p className="text-xl text-text-secondary max-w-2xl mx-auto leading-relaxed">
{t('home.description')}
</p>
{/* EN: Conditional rendering based on authentication status / VI: Render có điều kiện dựa trên trạng thái xác thực */}
<div className="pt-8">
{isAuthenticated && user ? (
// EN: Authenticated user welcome message / VI: Thông báo chào mừng người dùng đã xác thực
<div className="space-y-6 p-8 rounded-2xl bg-glass-bg backdrop-blur-glass border border-glass-border inline-block min-w-[320px] shadow-brand">
<div className="w-20 h-20 bg-brand-gradient rounded-full mx-auto flex items-center justify-center mb-6 text-3xl font-semibold text-white shadow-colored">
{user.email?.[0].toUpperCase()}
{/* EN: Conditional rendering based on authentication status / VI: Render có điều kiện dựa trên trạng thái xác thực */}
<div className="pt-8">
{isAuthenticated && user ? (
// EN: Authenticated user welcome message / VI: Thông báo chào mừng người dùng đã xác thực
<div className="space-y-6 p-8 rounded-2xl bg-glass-bg backdrop-blur-glass border border-glass-border inline-block min-w-[320px] shadow-brand">
<div className="w-20 h-20 bg-brand-gradient rounded-full mx-auto flex items-center justify-center mb-6 text-3xl font-semibold text-white shadow-colored">
{user.email?.[0].toUpperCase()}
</div>
<p className="text-lg font-medium text-text-primary">
{t('home.welcome', { email: user.email })}
</p>
<div className="inline-flex items-center px-4 py-2 rounded-full bg-bg-tertiary text-text-secondary text-sm border border-border-primary">
<span className="w-2 h-2 rounded-full bg-accent-success mr-2 animate-pulse" />
{t('home.role', { role: user.role })}
</div>
</div>
<p className="text-lg font-medium text-text-primary">
{t('home.welcome', { email: user.email })}
</p>
<div className="inline-flex items-center px-4 py-2 rounded-full bg-bg-tertiary text-text-secondary text-sm border border-border-primary">
<span className="w-2 h-2 rounded-full bg-accent-success mr-2 animate-pulse" />
{t('home.role', { role: user.role })}
) : (
// EN: Login prompt for unauthenticated users / VI: Nhắc đăng nhập cho người dùng chưa xác thực
<div className="space-y-8">
<p className="text-text-secondary text-lg">
{t('home.pleaseLogin')}
</p>
<div className="flex gap-4 justify-center flex-wrap">
<Button
variant="brand"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
>
Get Started
</Button>
<Button
variant="glass"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
>
Learn More
</Button>
</div>
</div>
</div>
) : (
// EN: Login prompt for unauthenticated users / VI: Nhắc đăng nhập cho người dùng chưa xác thực
<div className="space-y-8">
<p className="text-text-secondary text-lg">
{t('home.pleaseLogin')}
</p>
<div className="flex gap-4 justify-center flex-wrap">
<Button
variant="brand"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
)}
</div>
{/* EN: Feature highlights / VI: Điểm nổi bật tính năng */}
{!isAuthenticated && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-16 max-w-4xl mx-auto">
{[
{ title: 'Fast Development', icon: Zap, desc: 'Build and deploy in minutes' },
{ title: 'Enterprise Ready', icon: Building, desc: 'Production-grade platform' },
{ title: 'Developer First', icon: Code, desc: 'Built for modern teams' },
].map((feature, index) => (
<div
key={index}
className="p-6 rounded-xl bg-glass-bg backdrop-blur-glass border border-glass-border hover:border-brand-primary/30 transition-all duration-normal hover:shadow-brand group"
>
Get Started
</Button>
<Button
variant="glass"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
>
Learn More
</Button>
</div>
<div className="flex justify-center mb-4 transition-transform duration-normal group-hover:scale-110">
<feature.icon className="h-8 w-8 text-brand-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 text-text-primary">
{feature.title}
</h3>
<p className="text-sm text-text-secondary">{feature.desc}</p>
</div>
))}
</div>
)}
</div>
{/* EN: Feature highlights / VI: Điểm nổi bật tính năng */}
{!isAuthenticated && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-16 max-w-4xl mx-auto">
{[
{ title: 'Fast Development', icon: '⚡', desc: 'Build and deploy in minutes' },
{ title: 'Enterprise Ready', icon: '🏢', desc: 'Production-grade platform' },
{ title: 'Developer First', icon: '💻', desc: 'Built for modern teams' },
].map((feature, index) => (
<div
key={index}
className="p-6 rounded-xl bg-glass-bg backdrop-blur-glass border border-glass-border hover:border-brand-primary/30 transition-all duration-normal hover:shadow-brand group"
>
<div className="text-4xl mb-4 group-hover:scale-110 transition-transform duration-normal">
{feature.icon}
</div>
<h3 className="text-lg font-semibold mb-2 text-text-primary">
{feature.title}
</h3>
<p className="text-sm text-text-secondary">{feature.desc}</p>
</div>
))}
</div>
)}
</div>
</main>
</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,219 @@
'use client';
import React from 'react';
import { Github, Twitter, Linkedin, Mail } from 'lucide-react';
import { BrandLogo } from '../ui/brand-logo';
import { useTranslation } from '@/hooks/use-translation';
import { cn } from '@/lib/utils';
/**
* EN: Footer props
* VI: Props cho Footer
*/
export interface FooterProps {
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
}
/**
* EN: Footer - Responsive footer with brand elements, navigation links, and social media
* VI: Footer - Footer responsive với brand elements, navigation links và social media
*
* Features:
* - Responsive design (mobile: stacked, desktop: 4-column grid)
* - Brand logo and tagline
* - Navigation links (Product, Company, Support)
* - Social media links
* - Copyright notice with dynamic year
* - i18n support (EN/VI)
* - Accessibility (ARIA labels, semantic HTML)
*
* @example
* ```tsx
* <Footer />
* ```
*/
export function Footer({ className }: FooterProps) {
// EN: Translation hook / VI: Hook translation
const { t } = useTranslation();
// EN: Current year for copyright / VI: Năm hiện tại cho copyright
const currentYear = new Date().getFullYear();
// EN: Navigation link sections / VI: Các section navigation links
const navigationSections = [
{
title: t('footer.product'),
links: [
{ label: t('footer.features'), href: '#features' },
{ label: t('footer.pricing'), href: '#pricing' },
{ label: t('footer.documentation'), href: '#docs' },
],
},
{
title: t('footer.company'),
links: [
{ label: t('footer.about'), href: '#about' },
{ label: t('footer.blog'), href: '#blog' },
{ label: t('footer.careers'), href: '#careers' },
],
},
{
title: t('footer.support'),
links: [
{ label: t('footer.helpCenter'), href: '#help' },
{ label: t('footer.contact'), href: '#contact' },
{ label: t('footer.status'), href: '#status' },
],
},
];
// EN: Social media links / VI: Links mạng xã hội
const socialLinks = [
{ icon: Github, label: 'GitHub', href: 'https://github.com/goodgo', ariaLabel: 'Visit our GitHub' },
{ icon: Twitter, label: 'Twitter', href: 'https://twitter.com/goodgo', ariaLabel: 'Follow us on Twitter' },
{ icon: Linkedin, label: 'LinkedIn', href: 'https://linkedin.com/company/goodgo', ariaLabel: 'Connect on LinkedIn' },
{ icon: Mail, label: 'Email', href: 'mailto:hello@goodgo.com', ariaLabel: 'Send us an email' },
];
return (
<footer
className={cn(
'border-t border-border-primary bg-bg-secondary',
'transition-all duration-normal',
className
)}
>
<div className="container-responsive py-12">
{/* EN: Main footer content / VI: Nội dung footer chính */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
{/* EN: Column 1 - Brand / VI: Cột 1 - Thương hiệu */}
<div className="space-y-4">
<BrandLogo variant="wordmark" size="sm" />
<p className="text-sm text-text-secondary max-w-xs">
{t('footer.tagline')}
</p>
{/* EN: Social media links / VI: Links mạng xã hội */}
<div className="flex gap-3">
<p className="text-sm text-text-secondary sr-only">
{t('footer.followUs')}
</p>
{socialLinks.map((social) => {
const Icon = social.icon;
return (
<a
key={social.label}
href={social.href}
target="_blank"
rel="noopener noreferrer"
aria-label={social.ariaLabel}
className={cn(
'p-2 rounded-md',
'text-text-secondary hover:text-brand-primary',
'bg-bg-tertiary hover:bg-bg-primary',
'transition-colors duration-fast',
'focus-visible:outline-2 focus-visible:outline-accent-primary',
'btn-touch'
)}
>
<Icon className="h-5 w-5" />
</a>
);
})}
</div>
</div>
{/* EN: Columns 2-4 - Navigation sections / VI: Cột 2-4 - Các section navigation */}
{navigationSections.map((section) => (
<div key={section.title} className="space-y-4">
<h3 className="text-sm font-semibold text-text-primary uppercase tracking-wider">
{section.title}
</h3>
<ul className="space-y-3">
{section.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className={cn(
'text-sm text-text-secondary hover:text-brand-primary',
'transition-colors duration-fast',
'focus-visible:outline-2 focus-visible:outline-accent-primary',
'inline-block'
)}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
{/* EN: Bottom bar - Copyright and legal links / VI: Bottom bar - Copyright và legal links */}
<div className="border-t border-border-primary pt-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
{/* EN: Copyright notice / VI: Thông báo copyright */}
<p className="text-sm text-text-secondary text-center md:text-left">
{t('footer.copyright', { year: currentYear })}
</p>
{/* EN: Legal links / VI: Legal links */}
<div className="flex gap-6">
<a
href="#privacy"
className={cn(
'text-sm text-text-secondary hover:text-brand-primary',
'transition-colors duration-fast',
'focus-visible:outline-2 focus-visible:outline-accent-primary'
)}
>
{t('footer.privacyPolicy')}
</a>
<a
href="#terms"
className={cn(
'text-sm text-text-secondary hover:text-brand-primary',
'transition-colors duration-fast',
'focus-visible:outline-2 focus-visible:outline-accent-primary'
)}
>
{t('footer.termsOfService')}
</a>
</div>
</div>
</div>
</div>
</footer>
);
}
/**
* EN: Minimal Footer - Simplified version with just copyright
* VI: Footer tối giản - Phiên bản đơn giản chỉ có copyright
*
* @example
* ```tsx
* <MinimalFooter />
* ```
*/
export function MinimalFooter({ className }: FooterProps) {
const { t } = useTranslation();
const currentYear = new Date().getFullYear();
return (
<footer
className={cn(
'border-t border-border-primary bg-bg-secondary',
className
)}
>
<div className="container-responsive py-6">
<p className="text-sm text-text-secondary text-center">
{t('footer.copyright', { year: currentYear })}
</p>
</div>
</footer>
);
}

View File

@@ -3,7 +3,12 @@
import React from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from './button';
import { DropdownMenu } from './dropdown-menu';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem
} from './dropdown-menu';
import { cn } from '@/lib/utils';
/**
@@ -11,18 +16,17 @@ import { cn } from '@/lib/utils';
* 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' },
{ 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 with flag icons
* VI: Bộ chuyển ngôn ngữ - Hỗ trợ đa ngôn ngữ với flag icons
* EN: Language Switcher - Multi-language support
* VI: Bộ chuyển ngôn ngữ - Hỗ trợ đa ngôn ngữ
*
* Features:
* - Flag emoji icons for visual identification
* - Dropdown menu for language selection
* - Persists locale to localStorage
* - Updates URL locale parameter
@@ -73,29 +77,26 @@ export function LanguageSwitcher() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="glass"
size="md"
className="gap-2 min-w-[100px]"
className="gap-2 min-w-[80px]"
aria-label="Change language"
>
<span className="text-lg" role="img" aria-label={currentLanguage.label}>
{currentLanguage.flag}
</span>
<span className="hidden sm:inline">{currentLanguage.code.toUpperCase()}</span>
<span className="font-medium">{currentLanguage.code.toUpperCase()}</span>
</Button>
</DropdownMenu.Trigger>
</DropdownMenuTrigger>
<DropdownMenu.Content
<DropdownMenuContent
align="end"
className="bg-glass-bg backdrop-blur-glass border border-glass-border min-w-[180px]"
className="bg-glass-bg backdrop-blur-glass border border-glass-border min-w-[150px]"
>
{languages.map((language) => {
const isActive = currentLocale === language.code;
return (
<DropdownMenu.Item
<DropdownMenuItem
key={language.code}
onClick={() => handleLanguageChange(language.code)}
className={cn(
@@ -104,9 +105,6 @@ export function LanguageSwitcher() {
isActive && 'bg-brand-primary/10 text-brand-primary'
)}
>
<span className="text-lg" role="img" aria-label={language.label}>
{language.flag}
</span>
<div className="flex flex-col">
<span className="font-medium">{language.nativeName}</span>
<span className="text-xs text-text-tertiary">{language.label}</span>
@@ -114,10 +112,10 @@ export function LanguageSwitcher() {
{isActive && (
<span className="ml-auto text-brand-primary"></span>
)}
</DropdownMenu.Item>
</DropdownMenuItem>
);
})}
</DropdownMenu.Content>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -164,8 +162,8 @@ export function LanguageSwitcherCompact() {
onClick={toggleLanguage}
aria-label={`Switch to ${currentLocale === 'en' ? 'Vietnamese' : 'English'}`}
>
<span role="img" aria-label={currentLanguage.label}>
{currentLanguage.flag}
<span className="font-medium text-sm">
{currentLanguage.code.toUpperCase()}
</span>
</Button>
);

View File

@@ -4,7 +4,12 @@ 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 {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem
} from './dropdown-menu';
import { cn } from '@/lib/utils';
/**
@@ -37,7 +42,7 @@ export function ThemeToggle() {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="glass"
size="md"
@@ -46,9 +51,9 @@ export function ThemeToggle() {
>
<CurrentIcon className="h-5 w-5" />
</Button>
</DropdownMenu.Trigger>
</DropdownMenuTrigger>
<DropdownMenu.Content
<DropdownMenuContent
align="end"
className="bg-glass-bg backdrop-blur-glass border border-glass-border min-w-[150px]"
>
@@ -57,7 +62,7 @@ export function ThemeToggle() {
const isActive = theme === option.value;
return (
<DropdownMenu.Item
<DropdownMenuItem
key={option.value}
onClick={() => setTheme(option.value)}
className={cn(
@@ -71,10 +76,10 @@ export function ThemeToggle() {
{isActive && (
<span className="ml-auto text-brand-primary"></span>
)}
</DropdownMenu.Item>
</DropdownMenuItem>
);
})}
</DropdownMenu.Content>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -142,6 +142,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
// 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();
@@ -149,10 +151,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
}
};
if (typeof window !== 'undefined') {
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [toggleTheme]);
const value: ThemeContextValue = {

View File

@@ -396,5 +396,24 @@
"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"
}
}

View File

@@ -396,5 +396,24 @@
"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"
}
}