Spaces:
Configuration error
Configuration error
| "use client" | |
| import { useState, useRef } from "react" | |
| import Link from "next/link" | |
| import { Search, Sparkles, ChevronLeft, ChevronRight } from "lucide-react" | |
| import { ToolDefinition } from "@/lib/tools" | |
| import { DynamicIcon } from "@/components/ui/dynamic-icon" | |
| import { Button } from "@/components/ui/button" | |
| import { Input } from "@/components/ui/input" | |
| import { Badge } from "@/components/ui/badge" | |
| import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" | |
| import { cn } from "@/lib/utils" | |
| interface ToolsBrowserProps { | |
| tools: ToolDefinition[] | |
| categories: string[] | |
| } | |
| export function ToolsBrowser({ tools, categories }: ToolsBrowserProps) { | |
| const [selectedCategory, setSelectedCategory] = useState<string>("All") | |
| const [searchQuery, setSearchQuery] = useState("") | |
| const scrollContainerRef = useRef<HTMLDivElement>(null) | |
| const scroll = (direction: 'left' | 'right') => { | |
| if (scrollContainerRef.current) { | |
| const scrollAmount = 300 | |
| const currentScroll = scrollContainerRef.current.scrollLeft | |
| scrollContainerRef.current.scrollTo({ | |
| left: direction === 'left' ? currentScroll - scrollAmount : currentScroll + scrollAmount, | |
| behavior: 'smooth' | |
| }) | |
| } | |
| } | |
| // Filter tools based on search and category | |
| const filteredTools = tools.filter(tool => { | |
| const matchesCategory = selectedCategory === "All" || tool.category === selectedCategory | |
| const matchesSearch = tool.name.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| tool.description.toLowerCase().includes(searchQuery.toLowerCase()) | |
| return matchesCategory && matchesSearch | |
| }) | |
| // Group for "All" view | |
| const displayCategories = selectedCategory === "All" | |
| ? categories | |
| : [selectedCategory] | |
| return ( | |
| <div className="space-y-8"> | |
| {/* Controls Section */} | |
| <div className="flex flex-col md:flex-row gap-6 items-center justify-between"> | |
| {/* Search */} | |
| <div className="relative w-full md:w-96"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search tools..." | |
| className="pl-10 h-11 glass" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| </div> | |
| {/* Filter Pills - Managed Scroll */} | |
| <div className="relative w-full md:w-auto flex-1 min-w-0 flex items-center gap-2"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="hidden md:flex shrink-0 h-8 w-8 rounded-full" | |
| onClick={() => scroll('left')} | |
| > | |
| <ChevronLeft className="h-4 w-4" /> | |
| </Button> | |
| <div | |
| ref={scrollContainerRef} | |
| className="flex gap-2 overflow-x-auto pb-4 md:pb-0 scrollbar-hide mask-fade no-scrollbar" | |
| id="categories-container" | |
| style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }} | |
| > | |
| <Button | |
| variant={selectedCategory === "All" ? "gradient" : "outline"} | |
| size="sm" | |
| onClick={() => setSelectedCategory("All")} | |
| className={cn( | |
| "rounded-full whitespace-nowrap transition-all duration-300", | |
| selectedCategory === "All" ? "btn-glow" : "hover:bg-primary/10" | |
| )} | |
| > | |
| All Tools | |
| </Button> | |
| {categories.map((category) => ( | |
| <Button | |
| key={category} | |
| variant={selectedCategory === category ? "gradient" : "outline"} | |
| size="sm" | |
| onClick={() => setSelectedCategory(category)} | |
| className={cn( | |
| "rounded-full capitalize whitespace-nowrap transition-all duration-300", | |
| selectedCategory === category ? "btn-glow" : "hover:bg-primary/10" | |
| )} | |
| > | |
| {category} | |
| </Button> | |
| ))} | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="hidden md:flex shrink-0 h-8 w-8 rounded-full" | |
| onClick={() => scroll('right')} | |
| > | |
| <ChevronRight className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| {/* Tools Grid */} | |
| <div className="space-y-12"> | |
| {filteredTools.length === 0 ? ( | |
| <div className="text-center py-20 text-muted-foreground"> | |
| <div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4"> | |
| <Search className="h-6 w-6" /> | |
| </div> | |
| <p className="text-lg">No tools found matching your criteria.</p> | |
| <Button | |
| variant="link" | |
| onClick={() => { | |
| setSelectedCategory("All") | |
| setSearchQuery("") | |
| }} | |
| > | |
| Clear filters | |
| </Button> | |
| </div> | |
| ) : ( | |
| displayCategories.map((category) => { | |
| // In "All" view, valid tools are those belonging to the current category iteration | |
| // AND matching the search query. | |
| // In specific view, 'filteredTools' already contains only the right tools. | |
| const categoryTools = selectedCategory === "All" | |
| ? filteredTools.filter(t => t.category === category) | |
| : filteredTools | |
| if (categoryTools.length === 0) return null | |
| return ( | |
| <div key={category} className="animate-fade-in"> | |
| <h2 className="text-2xl font-serif font-medium mb-6 capitalize flex items-center gap-3"> | |
| <span className="text-gradient">{category}</span> | |
| <Badge variant="secondary" className="glass bg-primary/5 text-primary ml-2"> | |
| {categoryTools.length} | |
| </Badge> | |
| </h2> | |
| <div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5"> | |
| {categoryTools.map((tool) => ( | |
| <Link key={tool.slug} href={`/tools/${tool.slug}`}> | |
| <Card className="h-full glass hover:border-primary/50 transition-all cursor-pointer group hover:-translate-y-1 hover:shadow-lg hover:shadow-primary/5"> | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between mb-3"> | |
| <span className="bg-linear-to-br from-white/10 to-white/5 p-2 rounded-xl border border-white/5"> | |
| <DynamicIcon name={tool.icon} className="h-8 w-8 text-white" /> | |
| </span> | |
| {tool.isPremium && ( | |
| <Badge variant="default" className="bg-linear-to-r from-yellow-500 to-orange-500 shadow-lg shadow-orange-500/20"> | |
| PRO | |
| </Badge> | |
| )} | |
| </div> | |
| <CardTitle className="text-lg group-hover:text-primary transition-colors line-clamp-1"> | |
| {tool.name} | |
| </CardTitle> | |
| <CardDescription className="line-clamp-2 leading-relaxed"> | |
| {tool.description} | |
| </CardDescription> | |
| </CardHeader> | |
| </Card> | |
| </Link> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| }) | |
| )} | |
| </div> | |
| {/* Footer Stats */} | |
| <div className="mt-16 text-center border-t border-border/50 pt-8"> | |
| <div className="inline-flex items-center gap-2 px-6 py-3 bg-primary/5 rounded-full glass text-sm"> | |
| <Sparkles className="h-4 w-4 text-primary" /> | |
| <span className="font-medium text-muted-foreground"> | |
| Showing {filteredTools.length} of {tools.length} available tools | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |