view love/epi/src/components/Sidebar/Sidebar.tsx @ 139:e8f693bece90

test again
author June Park <parkjune1995@gmail.com>
date Fri, 09 Jan 2026 12:29:20 -0800
parents cf9caa4abc3e
children
line wrap: on
line source

import { useAtom } from 'jotai';
import { chatsAtom, currentChatId } from '@/atoms/chatAtoms';
import type { Chat } from '@/atoms/chatAtoms';
import { useEffect, useState } from 'react';
import { Loader2, MessageSquare, Plus, ChevronDown } from 'lucide-react';
import { apiUrl } from '@/utils';
import { useNavigate } from '@tanstack/react-router';

export function Sidebar() {
  const [chats, setChats] = useAtom(chatsAtom);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);
  const [currentChatIdValue, setCurrentChatId] = useAtom(currentChatId);
  const navigate = useNavigate();

  const LIMIT = 12;

  const fetchChats = async (currentCursor: string | null = null, append: boolean = false) => {
    try {
      if (append) {
        setLoadingMore(true);
      } else {
        setLoading(true);
      }
      setError(null);

      const url = currentCursor 
        ? apiUrl(`/chats?limit=${LIMIT}&cursor=${currentCursor}`)
        : apiUrl(`/chats?limit=${LIMIT}`);

      const res = await fetch(url);

      if (!res.ok) {
        throw new Error('Failed to fetch chats');
      }

      const data: Chat[] = (await res.json()).chats;
      
      if (data.length < LIMIT) {
        setHasMore(false);
      }

      if (data.length > 0) {
        setCursor(data[data.length - 1].id);
      }

      setChats((prev) => ({
        ...prev,
        ...data.reduce((acc, chat) => {
          acc[chat.id] = chat;
          return acc;
        }, {} as Record<string, Chat>),
      }));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Something went wrong');
      console.error('Error fetching chats:', err);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  };

  useEffect(() => {
    fetchChats();
  }, []);

  const loadMore = () => {
    if (!loadingMore && hasMore && cursor) {
      fetchChats(cursor, true);
    }
  };

  const chatList = Object.values(chats);

  if (loading) {
    return (
      <div className="w-[35%] h-full border-r border-gray-800 bg-gray-900 flex items-center justify-center">
        <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="w-[35%] h-full border-r border-gray-800 bg-gray-900 flex flex-col items-center justify-center p-4">
        <p className="text-red-400 text-sm text-center">Error: {error}</p>
        <button
          onClick={() => window.location.reload()}
          className="mt-3 px-4 py-2 text-xs font-medium text-white bg-gray-800 hover:bg-gray-700 rounded-md transition-colors"
        >
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="w-[35%] h-full flex flex-col border-r border-gray-800 bg-gray-900 text-gray-100 h-screen">
      
      {/* HEADER: Static height, does not scroll */}
      <div className="p-4 border-b border-gray-800 flex justify-between items-center shrink-0">
        <h2 className="text-lg font-semibold text-white tracking-tight">Chats</h2>
        <button 
          onClick={() => window.location.replace('/')}
          className="p-1.5 hover:bg-gray-800 rounded-md text-gray-400 hover:text-white transition-colors"
        >
          <Plus className="w-5 h-5" />
        </button>
      </div>

      <div className="flex-1 overflow-y-scroll">
        {chatList.length === 0 ? (
          <div className="flex flex-col items-center justify-center h-full text-gray-500">
            <MessageSquare className="w-10 h-10 mb-3 opacity-20" />
            <p className="text-sm font-medium">No chats yet</p>
          </div>
        ) : (
          <ul className="flex flex-col p-2 gap-1">
            {chatList.map((chat) => {
              const isActive = currentChatIdValue === chat.id;
              return (
                <li key={chat.id}>
                  <button
                    onClick={() => {
                      navigate(
                        { to: '/chat/$chatId', params: { chatId: chat.id } },
                      );
                      setCurrentChatId(chat.id);
                    }}
                    className={`w-full text-left px-3 py-3 rounded-lg transition-all h-[8%] duration-200 ease-in-out group ${
                      isActive
                        ? 'bg-gray-800 text-white'
                        : 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200'
                    }`}
                  >
                    <div className="flex justify-between items-center">
                      <div className="flex-1 min-w-0 pr-2">
                        <h3 className={`text-sm font-medium truncate ${isActive ? 'text-white' : 'text-gray-300 group-hover:text-white'}`}>
                          {chat.title || 'Untitled Chat'}
                        </h3>
                      </div>
                    </div>
                  </button>
                </li>
              );
            })}
            
            {hasMore && (
              <li className="pt-2 pb-4 px-2">
                <button
                  onClick={loadMore}
                  disabled={loadingMore}
                  className="w-full flex items-center justify-center gap-2 py-2 text-xs font-medium text-gray-500 hover:text-gray-300 transition-colors border border-dashed border-gray-700 rounded-md hover:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
                >
                  {loadingMore ? (
                    <>
                      <Loader2 className="w-3 h-3 animate-spin" />
                      Loading...
                    </>
                  ) : (
                    <>
                      Load More <ChevronDown className="w-3 h-3" />
                    </>
                  )}
                </button>
              </li>
            )}
          </ul>
        )}
      </div>
    </div>
  );
}