Files
pos-system/apps/web-client/src/features/chat/chat-layout.tsx

305 lines
11 KiB
TypeScript

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