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