Skip to main content
Glama
recommended-apps.tsx5.82 kB
"use client" import { McpApp } from "@repo/db/types" import { Badge } from "@repo/ui/components/ui/badge" import { Button } from "@repo/ui/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@repo/ui/components/ui/card" import { AnimatePresence,motion } from "framer-motion" import { ArrowRight, ChevronLeft, ChevronRight, Star } from "lucide-react" import Image from "next/image" import Link from "next/link" import { useCallback, useEffect,useState } from "react" import { trpc } from "@/lib/trpc/client" import { getAssetUrl } from "@/lib/utils" interface RecommendedAppsProps { currentApp: McpApp limit?: number } const useCarousel = (items: any[], itemsPerPage: number) => { const [currentIndex, setCurrentIndex] = useState(0) const totalPages = Math.ceil(items.length / itemsPerPage) const handlePrevious = useCallback(() => { setCurrentIndex((prev) => (prev > 0 ? prev - 1 : totalPages - 1)) }, [totalPages]) const handleNext = useCallback(() => { setCurrentIndex((prev) => (prev < totalPages - 1 ? prev + 1 : 0)) }, [totalPages]) const visibleItems = items.slice(currentIndex * itemsPerPage, (currentIndex + 1) * itemsPerPage) return { currentIndex, totalPages, handlePrevious, handleNext, visibleItems, } } const LoadingSkeleton = () => ( <div className="grid gap-6 md:grid-cols-3"> {[...Array(3)].map((_, i) => ( <motion.div key={i} initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="animate-pulse" > <div className="h-32 bg-muted rounded-lg" /> <div className="mt-4 space-y-2"> <div className="h-4 bg-muted rounded w-1/4" /> <div className="h-4 bg-muted rounded w-3/4" /> </div> </motion.div> ))} </div> ) const ErrorState = () => ( <div className="text-center text-muted-foreground"> 加载推荐应用时出现错误,请稍后再试 </div> ) export function RecommendedApps({ currentApp, limit = 6 }: RecommendedAppsProps) { const { data: recommendedApps = [], isLoading, error } = trpc.mcpRecommendations.getAppRecommendedApps.useQuery({ limit, appId: currentApp.id, }) const { currentIndex, totalPages, handlePrevious, handleNext, visibleItems } = useCarousel(recommendedApps, 3) if (isLoading) { return ( <Card className="mt-8 overflow-hidden"> <CardHeader className="bg-muted/30 pb-4"> <CardTitle>推荐应用</CardTitle> </CardHeader> <CardContent className="p-6"> <LoadingSkeleton /> </CardContent> </Card> ) } if (error) { return ( <Card className="mt-8 overflow-hidden"> <CardHeader className="bg-muted/30 pb-4"> <CardTitle>推荐应用</CardTitle> </CardHeader> <CardContent className="p-6"> <ErrorState /> </CardContent> </Card> ) } if (recommendedApps.length === 0) { return null } return ( <Card className="mt-8 overflow-hidden"> <CardHeader> <CardTitle>推荐应用</CardTitle> </CardHeader> <CardContent className="p-6"> <AnimatePresence mode="wait"> <motion.div key={currentIndex} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.3 }} className="grid gap-6 md:grid-cols-3" > {visibleItems.map((app) => ( <Link key={app.slug} href={`/apps/${app.slug}`} className="group block"> <motion.div whileHover={{ scale: 1.02 }} className="relative overflow-hidden rounded-lg border transition-all group-hover:shadow-md" > <div className="absolute inset-0 bg-gradient-to-b from-transparent to-background/90 z-10" /> <div className="h-32 bg-muted"> {app.icon ? ( <Image src={getAssetUrl(app.icon) || "/placeholder.svg"} alt={app.name} width={128} height={128} className="w-full h-full object-cover" /> ) : ( <div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/5 to-primary/30"> <span className="text-4xl font-bold text-primary/70">{app.name.charAt(0)}</span> </div> )} </div> <div className="relative z-20 p-4 -mt-8"> <div className="flex items-center justify-between mb-2"> <Badge variant={app.type === "client" ? "default" : app.type === "server" ? "secondary" : "outline"} className="text-xs" > {app.type === "client" ? "客户端" : app.type === "server" ? "服务器" : "应用"} </Badge> </div> <h3 className="font-medium mb-1 group-hover:text-primary transition-colors">{app.name}</h3> <p className="text-xs text-muted-foreground line-clamp-2">{app.description}</p> <div className="flex items-center justify-end mt-2 text-xs text-primary font-medium"> 查看详情 <ArrowRight className="ml-1 h-3 w-3" /> </div> </div> </motion.div> </Link> ))} </motion.div> </AnimatePresence> </CardContent> </Card> ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/metacode0602/open-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server