| import { useState, useMemo } from "react"; | |
| import { Navbar } from "@/components/Navbar"; | |
| import { PluginCard } from "@/components/PluginCard"; | |
| import { Footer } from "@/components/Footer"; | |
| import { StatsCard } from "@/components/StatsCard"; | |
| import { VisitorChart } from "@/components/VisitorChart"; | |
| import { usePlugins, useStats } from "@/client/hooks/usePlugin"; | |
| import { Activity, CheckCircle2, XCircle, TrendingUp, Loader2, Search, X } from "lucide-react"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Button } from "@/components/ui/button"; | |
| export default function Docs() { | |
| const { plugins, loading: pluginsLoading } = usePlugins(); | |
| const { stats, loading: statsLoading } = useStats(); | |
| const [selectedCategory, setSelectedCategory] = useState<string | null>(null); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [selectedTags, setSelectedTags] = useState<string[]>([]); | |
| const allTags = useMemo(() => { | |
| const tagsSet = new Set<string>(); | |
| plugins.forEach(plugin => { | |
| plugin.tags?.forEach(tag => tagsSet.add(tag)); | |
| }); | |
| return Array.from(tagsSet).sort(); | |
| }, [plugins]); | |
| const filteredPlugins = useMemo(() => { | |
| let filtered = plugins; | |
| if (selectedCategory) { | |
| filtered = filtered.filter((p) => p.category.includes(selectedCategory)); | |
| } | |
| if (searchQuery.trim()) { | |
| const query = searchQuery.toLowerCase(); | |
| filtered = filtered.filter((p) => | |
| p.name.toLowerCase().includes(query) || | |
| p.description.toLowerCase().includes(query) || | |
| p.endpoint.toLowerCase().includes(query) || | |
| p.tags?.some(tag => tag.toLowerCase().includes(query)) | |
| ); | |
| } | |
| if (selectedTags.length > 0) { | |
| filtered = filtered.filter((p) => | |
| selectedTags.every(tag => p.tags?.includes(tag)) | |
| ); | |
| } | |
| return filtered; | |
| }, [plugins, selectedCategory, searchQuery, selectedTags]); | |
| const toggleTag = (tag: string) => { | |
| setSelectedTags(prev => | |
| prev.includes(tag) | |
| ? prev.filter(t => t !== tag) | |
| : [...prev, tag] | |
| ); | |
| }; | |
| const clearAllFilters = () => { | |
| setSearchQuery(""); | |
| setSelectedTags([]); | |
| setSelectedCategory(null); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-background flex flex-col font-sans selection:bg-primary/30"> | |
| {/* Navbar with Categories in Hamburger Menu */} | |
| <Navbar onCategorySelect={setSelectedCategory} selectedCategory={selectedCategory} /> | |
| {/* Main Content */} | |
| <main className="flex-grow"> | |
| <div className="max-w-7xl mx-auto px-4 py-8"> | |
| {/* Statistics Cards */} | |
| <div className="mb-8"> | |
| <h2 className="text-2xl font-bold text-white mb-4">API Statistics</h2> | |
| {statsLoading ? ( | |
| <div className="flex items-center justify-center py-12"> | |
| <Loader2 className="w-8 h-8 text-purple-400 animate-spin" /> | |
| </div> | |
| ) : stats ? ( | |
| <> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> | |
| <StatsCard | |
| title="Total Requests" | |
| value={stats.totalRequests.toLocaleString()} | |
| icon={Activity} | |
| color="purple" | |
| /> | |
| <StatsCard | |
| title="Successful" | |
| value={stats.totalSuccess.toLocaleString()} | |
| icon={CheckCircle2} | |
| color="green" | |
| /> | |
| <StatsCard | |
| title="Failed" | |
| value={stats.totalFailed.toLocaleString()} | |
| icon={XCircle} | |
| color="red" | |
| /> | |
| <StatsCard | |
| title="Success Rate" | |
| value={`${stats.successRate}%`} | |
| icon={TrendingUp} | |
| color="blue" | |
| /> | |
| </div> | |
| {/* Visitor Chart */} | |
| <VisitorChart /> | |
| </> | |
| ) : ( | |
| <div className="text-sm text-gray-500">Failed to load statistics</div> | |
| )} | |
| </div> | |
| {/* Search and Filter Section */} | |
| <div className="mb-6 space-y-4"> | |
| {/* Search Bar */} | |
| <div className="relative"> | |
| <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" /> | |
| <Input | |
| type="text" | |
| placeholder="Search endpoints, descriptions, or tags..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-10 bg-white/[0.02] border-white/10 text-white placeholder:text-gray-500 focus:border-purple-500 h-12" | |
| /> | |
| {searchQuery && ( | |
| <button | |
| onClick={() => setSearchQuery("")} | |
| className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white" | |
| > | |
| <X className="w-4 h-4" /> | |
| </button> | |
| )} | |
| </div> | |
| {/* Tags Filter */} | |
| {allTags.length > 0 && ( | |
| <div> | |
| <div className="flex items-center justify-between mb-2"> | |
| <h3 className="text-sm font-semibold text-gray-400">Filter by Tags</h3> | |
| {selectedTags.length > 0 && ( | |
| <button | |
| onClick={() => setSelectedTags([])} | |
| className="text-xs text-purple-400 hover:text-purple-300" | |
| > | |
| Clear tags | |
| </button> | |
| )} | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {allTags.map((tag) => ( | |
| <Badge | |
| key={tag} | |
| onClick={() => toggleTag(tag)} | |
| className={`cursor-pointer transition-colors ${ | |
| selectedTags.includes(tag) | |
| ? "bg-purple-500/30 text-purple-300 border-purple-500 hover:bg-purple-500/40" | |
| : "bg-white/5 text-gray-400 border-white/10 hover:bg-white/10" | |
| } border`} | |
| > | |
| {tag} | |
| </Badge> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Active Filters Summary */} | |
| {(selectedCategory || searchQuery || selectedTags.length > 0) && ( | |
| <div className="flex items-center gap-2 flex-wrap"> | |
| <span className="text-sm text-gray-400">Active filters:</span> | |
| {selectedCategory && ( | |
| <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/50"> | |
| Category: {selectedCategory} | |
| </Badge> | |
| )} | |
| {searchQuery && ( | |
| <Badge className="bg-green-500/20 text-green-400 border-green-500/50"> | |
| Search: "{searchQuery}" | |
| </Badge> | |
| )} | |
| {selectedTags.map(tag => ( | |
| <Badge key={tag} className="bg-purple-500/20 text-purple-400 border-purple-500/50"> | |
| Tag: {tag} | |
| </Badge> | |
| ))} | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={clearAllFilters} | |
| className="text-xs text-gray-400 hover:text-white" | |
| > | |
| Clear all | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Results Count */} | |
| <div className="mb-6"> | |
| <h2 className="text-2xl font-bold text-white capitalize"> | |
| {selectedCategory ? `${selectedCategory} Endpoints` : "All Endpoints"} | |
| </h2> | |
| <p className="text-gray-400 text-sm mt-1"> | |
| Showing {filteredPlugins.length} of {plugins.length} endpoint{filteredPlugins.length !== 1 ? 's' : ''} | |
| </p> | |
| </div> | |
| {/* Plugins List */} | |
| <div className="space-y-6"> | |
| {pluginsLoading ? ( | |
| <div className="flex items-center justify-center py-20"> | |
| <Loader2 className="w-8 h-8 text-purple-400 animate-spin" /> | |
| </div> | |
| ) : filteredPlugins.length > 0 ? ( | |
| filteredPlugins.map((plugin) => ( | |
| <PluginCard key={plugin.endpoint} plugin={plugin} /> | |
| )) | |
| ) : ( | |
| <div className="text-center py-20"> | |
| <div className="text-gray-400 text-lg mb-2">No endpoints found</div> | |
| <div className="text-gray-600 text-sm mb-4"> | |
| {searchQuery || selectedTags.length > 0 | |
| ? "Try adjusting your search or filters" | |
| : selectedCategory | |
| ? "No plugins available in this category" | |
| : "No plugins available"} | |
| </div> | |
| {(searchQuery || selectedTags.length > 0 || selectedCategory) && ( | |
| <Button | |
| onClick={clearAllFilters} | |
| variant="outline" | |
| className="border-white/10 text-purple-400 hover:bg-purple-500/10" | |
| > | |
| Clear all filters | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </main> | |
| <Footer /> | |
| </div> | |
| ); | |
| } |