File "ChatSidebar.tsx"
Full Path: /home/trinadezambia/public_html/student_panel/src/components/chat/ChatSidebar.tsx
File size: 12.54 KB
MIME-type: text/x-java
Charset: utf-8
'use client';
// app/components/chat/ChatSidebar.tsx
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { ChatSidebarItem } from './ChatSidebarItem';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { BiMessageAdd, BiSearch } from 'react-icons/bi';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import {
useGetChatUsersInfinite,
useGetChatHistoryInfinite,
} from '@/lib/api/student/queryHooks';
import type { RootState } from '../store';
import Image from 'next/image';
import { useTranslate } from '@/components/hooks/useTranslate';
import { useLanguage } from '@/components/hooks/useLanguage';
/**
* Utility function to extract time from date-time string
* Input format: "26/09/2025 11:52"
* Output format: "11:52"
*/
const extractTime = (dateTimeString: string): string => {
// Split the string by space to separate date and time
const parts = dateTimeString.split(' ');
// Return the time part (second element)
return parts.length > 1 ? parts[1] : dateTimeString;
};
// Define the contact type
type Contact = {
id: number;
name: string;
lastMessage: string;
time: string;
avatar: string;
unread: number;
active: boolean;
subject?: string;
};
// Props for ChatSidebar
type ChatSidebarProps = {
onContactSelect: (contact: Contact) => void;
selectedContact: Contact | null;
initialSelectedId?: number;
};
export const ChatSidebar = ({
onContactSelect,
selectedContact,
initialSelectedId,
}: ChatSidebarProps) => {
const translate = useTranslate();
const { currentLanguage } = useLanguage();
const isRTL = currentLanguage === 'ar';
// State to manage search input value
// This will be used to filter chat history by contact name or message content
const [searchTerm, setSearchTerm] = useState<string>('');
// Get the logged-in student's ID from Redux store for the API
const user = useSelector((state: RootState) => state.studentAuth.user);
// Fetch chat users (teachers/staff) using Infinite Query
// We use the new /users endpoint which supports pagination
const {
data: usersData,
fetchNextPage: fetchNextUsersPage,
hasNextPage: hasNextUsersPage,
isFetchingNextPage: isFetchingNextUsersPage,
isLoading: isLoadingUsers,
error: usersError,
} = useGetChatUsersInfinite('Staff', user?.id || null);
// Fetch chat history using Infinite Query
const {
data: chatHistoryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: isLoadingChatHistory,
error: chatHistoryError,
} = useGetChatHistoryInfinite('Staff', searchTerm);
// Flatten the paginated users data
const teachers =
usersData?.pages.flatMap((page) =>
Array.isArray(page.data?.data) ? page.data.data : [],
) || [];
// Flatten the paginated data
const chatHistory =
chatHistoryData?.pages.flatMap((page) =>
Array.isArray(page.data?.data) ? page.data.data : [],
) || [];
// Transform chat history to Contact format for ContactItem component
const contacts: Contact[] = chatHistory.map((chat) => {
// Determine the name to display
const displayName = chat.user.full_name;
// Determine the avatar to display
const displayAvatar = chat.user.image || '';
const contact: Contact = {
id: chat.user.id,
name: displayName,
lastMessage: chat?.last_message || '', // Use exact message as per requirement, handle missing messages
time: chat?.updated_at ? extractTime(chat.updated_at) : '',
avatar: displayAvatar,
unread: chat?.unread_count,
active: selectedContact?.id === chat.user.id,
subject:
chat.user.subject_teachers && chat.user.subject_teachers.length > 0
? chat.user.subject_teachers[0].subject_with_name
: undefined,
};
return contact;
});
// Handle contact selection
const handleContactClick = (contact: Contact) => {
onContactSelect(contact);
};
// Handle teacher selection
const handleTeacherSelect = (teacher: (typeof teachers)[0]) => {
// Create a contact object for the selected teacher
const newContact: Contact = {
id: teacher.id,
name: teacher.full_name,
lastMessage: translate('startConversation'),
time: 'now',
avatar: teacher.image || '',
unread: 0,
active: true,
subject:
teacher.subject_teachers && teacher.subject_teachers.length > 0
? teacher.subject_teachers[0].subject_with_name
: undefined,
};
onContactSelect(newContact);
};
// Auto-select contact if initialSelectedId is present and not already selected
useEffect(() => {
if (initialSelectedId && contacts.length > 0 && !selectedContact) {
const foundContact = contacts.find((c) => c.id === initialSelectedId);
if (foundContact) {
onContactSelect(foundContact);
}
}
}, [initialSelectedId, contacts, selectedContact, onContactSelect]);
return (
<aside className="w-full lg:w-[360px] bg-white border-e border-gray-200 flex flex-col h-full overflow-hidden">
<div className="p-3 md:p-4 border-b border-gray-200">
<div className="flex items-center justify-between gap-2 md:gap-4">
{/* Search Input - Responsive width */}
<div className="relative flex-1 bg-(--light-primary-color)">
<Input
type="text"
placeholder={translate('searchContact')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pe-10 rounded-[4px] h-[40px] md:h-[44px] shadow-none text-sm"
/>
<Button className="absolute end-[6px] top-1/2 transform -translate-y-1/2 h-6 w-6 md:h-7 md:w-7 bg-transparent hover:bg-transparent p-0">
<BiSearch className="text-lg md:text-xl text-gray-500" />
</Button>
</div>
{/* Teacher Selection Dropdown - Responsive button */}
<DropdownMenu dir={isRTL ? 'rtl' : 'ltr'}>
<DropdownMenuTrigger asChild>
<Button className="bg-(--primary-color) rounded-[4px] hover:bg-(--primary-color)/90 transition-colors h-[40px] w-[40px] md:h-[44px] md:w-[44px] p-0 flex items-center justify-center">
<BiMessageAdd className="w-6 h-6 md:w-6 md:h-6 text-white" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align={isRTL ? 'start' : 'end'}
className="w-72 max-h-96 overflow-y-auto p-1"
>
{/* Show loading state while fetching teachers */}
{isLoadingUsers && (
<div className="p-4 text-center text-sm text-gray-500">
{translate('loadingTeachers')}
</div>
)}
{/* Show error message if API call fails */}
{usersError && (
<div className="p-4 text-center text-sm text-red-500">
{translate('failedToLoadTeachers')}
</div>
)}
{/* Show teachers list when data is available */}
{!isLoadingUsers && !usersError && teachers.length > 0 && (
<>
{teachers.map((teacher, index) => (
<div key={teacher.id}>
<DropdownMenuItem
onClick={() => handleTeacherSelect(teacher)}
className="flex items-center p-2 cursor-pointer"
>
{/* Avatar - show image if available, otherwise show initials */}
<div className="w-7 h-7 md:w-8 md:h-8 bg-gray-200 rounded-full flex items-center justify-center text-xs font-medium text-gray-600 me-2 md:me-3">
{teacher.image ? (
<Image
src={teacher.image}
alt={teacher.full_name}
width={32}
height={32}
className="rounded-full w-full h-full object-cover"
/>
) : (
<span className="text-xs font-semibold">
{teacher.full_name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()}
</span>
)}
</div>
{/* Teacher info */}
<div className="flex-1">
<div className="font-medium text-gray-900 text-xs md:text-sm">
{teacher.full_name}
</div>
<div className="text-xs text-gray-500">
{teacher.role}
</div>
</div>
</DropdownMenuItem>
{/* Only show separator if not the last item */}
{index < teachers.length - 1 && <DropdownMenuSeparator />}
</div>
))}
{/* Load More Button for Teachers */}
{hasNextUsersPage && (
<div className="p-2 text-center border-t border-gray-100">
<Button
onClick={(e) => {
e.preventDefault(); // Prevent dropdown closing
fetchNextUsersPage();
}}
disabled={isFetchingNextUsersPage}
variant="ghost"
size="sm"
className="w-full text-xs h-8 text-primary hover:text-primary/80 hover:bg-primary/5"
>
{isFetchingNextUsersPage
? translate('loadingMore')
: translate('loadMore')}
</Button>
</div>
)}
</>
)}
{/* Show empty state when no teachers found */}
{!isLoadingUsers && !usersError && teachers.length === 0 && (
<div className="p-4 text-center text-sm text-gray-500">
{translate('noTeachersAvailable')}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grow overflow-y-auto">
<nav>
{/* Show contacts list when data is available */}
{!isLoadingChatHistory &&
!chatHistoryError &&
contacts.length > 0 && (
<>
{contacts.map((contact) => (
<ChatSidebarItem
key={contact.id}
{...contact}
isSelected={selectedContact?.id === contact.id}
onClick={() => handleContactClick(contact)}
/>
))}
{/* Load More Button */}
{hasNextPage && (
<div className="p-4 text-center w-full">
<Button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
variant="outline"
className="w-full text-xs h-8 text-primary border-primary hover:bg-primary/10"
>
{isFetchingNextPage
? translate('loadingMore') + '...'
: translate('loadMore')}
</Button>
</div>
)}
</>
)}
{/* Show loading state while fetching initial data */}
{isLoadingChatHistory && (
<div className="p-4 text-center text-sm text-gray-500">
{translate('loadingContacts')}
</div>
)}
{/* Show error message if API call fails */}
{chatHistoryError && (
<div className="p-4 text-center text-sm text-red-500">
{translate('failedToLoadContacts')}
</div>
)}
{/* Show empty state when no contacts found */}
{!isLoadingChatHistory &&
!chatHistoryError &&
contacts.length === 0 && (
<div className="p-4 text-center text-sm text-gray-500">
{translate('noContactsAvailable')}
</div>
)}
</nav>
</div>
</aside>
);
};