feat: add project search and scrollable thread lists

- Add search bar to filter projects by name in real-time
- Implement scrollable thread container with max 4 visible threads
- Add empty state for no search results
- Add clear button (X) to reset search query
This commit is contained in:
Roushan Singh 2025-10-03 16:20:51 +05:30 committed by Faisal Amir
parent 73b241c16f
commit 3e332eceae
2 changed files with 58 additions and 3 deletions

View File

@ -284,7 +284,10 @@
"updated": "Updated:", "updated": "Updated:",
"collapseThreads": "Collapse threads", "collapseThreads": "Collapse threads",
"expandThreads": "Expand threads", "expandThreads": "Expand threads",
"update": "Update" "update": "Update",
"searchProjects": "Search projects...",
"noProjectsFound": "No projects found",
"tryDifferentSearch": "Try a different search term"
}, },
"toast": { "toast": {
"allThreadsUnfavorited": { "allThreadsUnfavorited": {

View File

@ -14,6 +14,8 @@ import {
IconFolder, IconFolder,
IconChevronDown, IconChevronDown,
IconChevronRight, IconChevronRight,
IconSearch,
IconX,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import AddProjectDialog from '@/containers/dialogs/AddProjectDialog' import AddProjectDialog from '@/containers/dialogs/AddProjectDialog'
import { DeleteProjectDialog } from '@/containers/dialogs/DeleteProjectDialog' import { DeleteProjectDialog } from '@/containers/dialogs/DeleteProjectDialog'
@ -42,6 +44,7 @@ function ProjectContent() {
const [expandedProjects, setExpandedProjects] = useState<Set<string>>( const [expandedProjects, setExpandedProjects] = useState<Set<string>>(
new Set() new Set()
) )
const [searchQuery, setSearchQuery] = useState('')
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
setDeletingId(id) setDeletingId(id)
@ -93,6 +96,16 @@ function ProjectContent() {
}) })
} }
// Filter projects based on search query
const filteredProjects = useMemo(() => {
if (!searchQuery.trim()) {
return folders
}
return folders.filter((folder) =>
folder.name.toLowerCase().includes(searchQuery.toLowerCase())
)
}, [folders, searchQuery])
return ( return (
<div className="flex h-full flex-col justify-center"> <div className="flex h-full flex-col justify-center">
<HeaderPage> <HeaderPage>
@ -113,6 +126,33 @@ function ProjectContent() {
</HeaderPage> </HeaderPage>
<div className="h-full overflow-y-auto flex flex-col"> <div className="h-full overflow-y-auto flex flex-col">
<div className="p-4 w-full md:w-3/4 mx-auto mt-2"> <div className="p-4 w-full md:w-3/4 mx-auto mt-2">
{/* Search Bar */}
{folders.length > 0 && (
<div className="mb-4">
<div className="relative">
<IconSearch
size={18}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-main-view-fg/50"
/>
<input
type="text"
placeholder={t('projects.searchProjects')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-main-view-fg/5 border border-main-view-fg/10 rounded-lg text-main-view-fg placeholder:text-main-view-fg/50 focus:outline-none focus:ring-2 focus:ring-main-view-fg/20 focus:border-main-view-fg/20 transition-all"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-main-view-fg/50 hover:text-main-view-fg transition-colors"
>
<IconX size={18} />
</button>
)}
</div>
</div>
)}
{folders.length === 0 ? ( {folders.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <div className="flex flex-col items-center justify-center py-12 text-center">
<IconFolder size={48} className="text-main-view-fg/30 mb-4" /> <IconFolder size={48} className="text-main-view-fg/30 mb-4" />
@ -123,9 +163,19 @@ function ProjectContent() {
{t('projects.noProjectsYetDesc')} {t('projects.noProjectsYetDesc')}
</p> </p>
</div> </div>
) : filteredProjects.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<IconSearch size={48} className="text-main-view-fg/30 mb-4" />
<h3 className="text-lg font-medium text-main-view-fg/60 mb-2">
{t('projects.noProjectsFound')}
</h3>
<p className="text-main-view-fg/50 text-sm">
{t('projects.tryDifferentSearch')}
</p>
</div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{folders {filteredProjects
.slice() .slice()
.sort((a, b) => b.updated_at - a.updated_at) .sort((a, b) => b.updated_at - a.updated_at)
.map((folder) => { .map((folder) => {
@ -218,7 +268,9 @@ function ProjectContent() {
{/* Thread List */} {/* Thread List */}
{isExpanded && projectThreads.length > 0 && ( {isExpanded && projectThreads.length > 0 && (
<div className="mt-3 pl-2"> <div
className="mt-3 pl-2 pr-2 max-h-[190px] overflow-y-auto overflow-x-hidden [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-main-view-fg/20 [&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-main-view-fg/30"
>
<ThreadList <ThreadList
threads={projectThreads} threads={projectThreads}
variant="project" variant="project"