tahamajs/IE / Projects /front /src /components /threads /ThreadSidebar.tsx
tahamajs's picture
download
raw
7.18 kB
// src/components/threads/ThreadSidebar.tsx
import { useState, useMemo } from "react";
import type { Thread, ThreadFilters } from "@/types/thread";
import { ThreadBadge } from "./ThreadBadge";
import { ThreadParticipantAvatars } from "./ThreadParticipantAvatars";
import {
Search, Plus, Filter, Pin, Archive, CheckCircle2, X,
MessageSquare, Hash, Lightbulb, Loader2,
} from "lucide-react";
interface ThreadSidebarProps {
threads: Thread[];
activeThreadId: string | null;
unreadTotal: number;
filters: ThreadFilters;
onFilterChange: (filters: ThreadFilters) => void;
onSelectThread: (threadId: string) => void;
onCreateThread: () => void;
loading: boolean;
}
const STATUS_FILTERS: { status: ThreadFilters["status"]; icon: typeof Pin; label: string }[] = [
{ status: undefined, icon: MessageSquare, label: "All" },
{ status: ["pinned"], icon: Pin, label: "Pinned" },
{ status: ["active"], icon: MessageSquare, label: "Active" },
{ status: ["resolved"], icon: CheckCircle2, label: "Resolved" },
{ status: ["archived"], icon: Archive, label: "Archived" },
];
export function ThreadSidebar({
threads,
activeThreadId,
unreadTotal,
filters,
onFilterChange,
onSelectThread,
onCreateThread,
loading,
}: ThreadSidebarProps) {
const [searchOpen, setSearchOpen] = useState(false);
const activeStatus = filters.status?.[0] ?? undefined;
// Filter threads locally (status & search)
const filteredThreads = useMemo(() => {
let result = threads;
if (activeStatus) {
result = result.filter(t => t.status === activeStatus);
}
if (filters.searchQuery) {
const q = filters.searchQuery.toLowerCase();
result = result.filter(t => t.title.toLowerCase().includes(q));
}
return result;
}, [threads, activeStatus, filters.searchQuery]);
return (
<div className="flex h-full w-80 flex-col border-r border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-100 p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
<h2 className="text-sm font-bold">Threads</h2>
{unreadTotal > 0 && (
<span className="rounded-full bg-red-500 px-1.5 py-0.5 text-[10px] text-white">
{unreadTotal}
</span>
)}
</div>
<button
onClick={onCreateThread}
className="rounded-lg bg-primary p-1.5 text-white transition hover:bg-primary/90"
aria-label="Create new thread"
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* Search */}
<div className="p-2">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Search threads..."
value={filters.searchQuery ?? ""}
onChange={(e) =>
onFilterChange({ ...filters, searchQuery: e.target.value || undefined })
}
className="w-full rounded-lg bg-gray-100 py-1.5 pl-7 pr-3 text-xs focus:outline-none focus:ring-2 focus:ring-primary dark:bg-gray-700"
/>
</div>
</div>
{/* Status filter pills */}
<div className="flex gap-1 overflow-x-auto px-2 pb-2">
{STATUS_FILTERS.map((sf) => (
<button
key={sf.label}
onClick={() =>
onFilterChange({ ...filters, status: sf.status })
}
className={`flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-[10px] font-medium transition ${
(activeStatus === sf.status?.[0]) || (!activeStatus && sf.status === undefined)
? "bg-primary text-white"
: "bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-700"
}`}
>
<sf.icon className="h-3 w-3" />
{sf.label}
</button>
))}
</div>
{/* Thread list */}
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center p-4 text-center text-sm text-gray-400">
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading threads...
</div>
) : filteredThreads.length === 0 ? (
<div className="p-6 text-center text-gray-400">
<Hash className="mx-auto mb-2 h-8 w-8 opacity-30" />
<p className="text-sm">No threads found</p>
<button onClick={onCreateThread} className="mt-1 text-xs text-primary hover:underline">
Start the first thread
</button>
</div>
) : (
filteredThreads.map((thread) => (
<button
key={thread.id}
onClick={() => onSelectThread(thread.id)}
className={`w-full border-b border-gray-50 px-3 py-3 text-left transition hover:bg-gray-50 dark:border-gray-700/50 dark:hover:bg-gray-700/30 ${
activeThreadId === thread.id
? "border-l-2 border-l-primary bg-primary/5 dark:bg-primary/10"
: ""
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
{thread.status === "pinned" && (
<Pin className="h-3 w-3 text-yellow-500" />
)}
<h4 className="truncate text-sm font-semibold text-gray-800 dark:text-gray-200">
{thread.title}
</h4>
</div>
<p className="mt-0.5 text-[11px] text-gray-400">
{thread.messageCount} messages ·{" "}
{new Date(thread.lastActivityAt).toLocaleDateString()}
</p>
{thread.participantIds && thread.participantIds.length > 0 && (
<div className="mt-1">
<ThreadParticipantAvatars
participants={thread.participantIds.map((id) => ({
id,
username: `User ${id}`,
}))}
max={3}
/>
</div>
)}
</div>
{thread.unreadCount > 0 && (
<ThreadBadge count={thread.unreadCount} />
)}
</div>
{thread.tags && thread.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
{thread.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="rounded-full bg-gray-100 px-1.5 py-0.5 text-[9px] text-gray-500 dark:bg-gray-700"
>
{tag}
</span>
))}
</div>
)}
</button>
))
)}
</div>
</div>
);
}

Xet Storage Details

Size:
7.18 kB
·
Xet hash:
70455ac1692b78ae2715405e84c4942c5faaf0591092c806907ab4bea3bf3c83

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.