Docs: Revamp extensions site (#1260)

Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
This commit is contained in:
Adewale Abati
2025-03-24 14:56:40 +01:00
committed by GitHub
parent eb9b7fa5ee
commit bf760fd630
56 changed files with 2019 additions and 1029 deletions
@@ -0,0 +1,259 @@
import Layout from "@theme/Layout";
import { Download, Terminal, Star, ArrowLeft, Info } from "lucide-react";
import { Button } from "@site/src/components/ui/button";
import { Badge } from "@site/src/components/ui/badge";
import { getGooseInstallLink } from "@site/src/utils/install-links";
import { useLocation } from "@docusaurus/router";
import { useEffect, useState } from "react";
import type { MCPServer } from "@site/src/types/server";
import { fetchMCPServers } from "@site/src/utils/mcp-servers";
import Link from "@docusaurus/Link";
function ExtensionDetail({ server }: { server: MCPServer }) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="card-header mb-6">
<div className="flex items-center gap-4">
<h1 className="font-medium text-5xl text-textProminent m-0">
{server.name}
</h1>
{server.is_builtin && (
<Badge variant="secondary" className="text-sm">
Built-in
</Badge>
)}
</div>
</div>
<div className="card-content space-y-6">
<div>
<p className="text-xl text-textSubtle m-0">
{server.description}
</p>
</div>
{server.installation_notes && (
<div>
<p className="text-md text-textSubtle m-0">
{server.installation_notes}
</p>
</div>
)}
<div className="space-y-2">
{server.is_builtin ? (
<div className="flex items-center gap-2">
<Info className="h-4 w-4 text-textSubtle shrink-0" />
<span className="text-sm text-textSubtle">
Can be enabled in the goose settings page
</span>
</div>
) : (
<>
<div className="flex items-center gap-2 text-textStandard">
<Terminal className="h-4 w-4" />
<h4 className="font-medium m-0">Command</h4>
</div>
<div className="command-content">
<code className="text-sm block">
{`goose session --with-extension "${server.command}"`}
</code>
</div>
</>
)}
</div>
{server.environmentVariables && (
<div className="space-y-4">
<h2 className="text-lg font-medium text-textStandard m-0">
Environment Variables
</h2>
{server.environmentVariables.length > 0 ? (
<div>
{server.environmentVariables.map((env) => (
<div
key={env.name}
className="border-b border-borderSubtle py-4 first:pt-0 last:border-0"
>
<div className="text-sm text-textStandard font-medium">
{env.name}
</div>
<div className="text-textSubtle text-sm mt-1">
{env.description}
</div>
{env.required && (
<Badge variant="secondary" className="mt-2">
Required
</Badge>
)}
</div>
))}
</div>
) : (
<div className="text-textSubtle text-sm flex items-center gap-2">
<Info className="h-4 w-4" />
No environment variables needed
</div>
)}
</div>
)}
<div className="card-footer">
<a
href={server.link}
target="_blank"
rel="noopener noreferrer"
className="card-stats"
>
<Star className="h-4 w-4" />
<span>{server.githubStars} on Github</span>
</a>
{server.is_builtin ? (
<div
className="built-in-badge"
title="This extension is built into goose and can be enabled in the settings page"
>
Built-in
</div>
) : (
<a
href={getGooseInstallLink(server)}
target="_blank"
rel="noopener noreferrer"
className="install-button"
>
<span>Install</span>
<Download className="h-4 w-4" />
</a>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
export default function DetailPage(): JSX.Element {
const location = useLocation();
const [server, setServer] = useState<MCPServer | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadServer = async () => {
try {
setLoading(true);
setError(null);
const servers = await fetchMCPServers();
// Get the ID from the query parameter
const params = new URLSearchParams(location.search);
const id = params.get("id");
if (!id) {
setError("No extension ID provided");
return;
}
const foundServer = servers.find((s) => s.id === id);
if (foundServer) {
setServer(foundServer);
} else {
setError("Extension not found");
}
} catch (err) {
setError("Failed to load extension details");
console.error(err);
} finally {
setLoading(false);
}
};
loadServer();
}, [location]);
if (loading) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="animate-pulse">
<div className="h-12 w-48 bg-bgSubtle rounded-lg mb-4"></div>
<div className="h-6 w-full bg-bgSubtle rounded-lg mb-2"></div>
<div className="h-6 w-2/3 bg-bgSubtle rounded-lg"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
if (error || !server) {
return (
<Layout>
<div className="min-h-screen flex items-start justify-center py-16">
<div className="container max-w-5xl mx-auto px-4">
<div className="flex gap-8">
<div>
<Link to="/extensions" className="no-underline">
<Button
variant="ghost"
className="flex items-center gap-2 text-textSubtle hover:text-textProminent"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</Link>
</div>
<div className="server-card flex-1">
<div className="card p-8">
<div className="text-red-500">
{error || "Extension not found"}
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
return <ExtensionDetail server={server} />;
}
@@ -0,0 +1,118 @@
import { Search } from "lucide-react";
import { Button } from "@site/src/components/ui/button";
import { Input } from "@site/src/components/ui/input";
import { ServerCard } from "@site/src/components/server-card";
import { useState, useEffect } from "react";
import type { MCPServer } from "@site/src/types/server";
import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers";
import { motion } from "framer-motion";
import Layout from "@theme/Layout";
export default function HomePage() {
const [servers, setServers] = useState<MCPServer[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Combined effect for initial load and search
useEffect(() => {
const loadServers = async () => {
try {
setIsLoading(true);
setError(null);
const trimmedQuery = searchQuery.trim();
const results = trimmedQuery
? await searchMCPServers(trimmedQuery)
: await fetchMCPServers();
console.log("Loaded servers:", results);
setServers(results);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Unknown error";
setError(`Failed to load servers: ${errorMessage}`);
console.error("Error loading servers:", err);
} finally {
setIsLoading(false);
}
};
// Debounce all server loads
const timeoutId = setTimeout(loadServers, 300);
return () => clearTimeout(timeoutId);
}, [searchQuery]);
return (
<Layout>
<div className="container mx-auto px-4 p-24">
<div className="pb-16">
<h1 className="text-[64px] font-medium text-textProminent">
Browse Extensions
</h1>
<p className="text-textProminent">
Your central directory for discovering and installing extensions.
</p>
</div>
<div className="search-container">
<input
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-3 text-[40px] leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
placeholder="Search for extensions"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">{error}</div>
)}
<section className="">
<div className={`${searchQuery ? "pb-2" : "pb-8"}`}>
<p className="text-gray-600">
{searchQuery
? `${servers.length} result${
servers.length > 1 ? "s" : ""
} for "${searchQuery}"`
: ""}
</p>
</div>
{isLoading ? (
<div className="py-8 text-xl text-gray-600">Loading servers...</div>
) : servers.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{searchQuery
? "No servers found matching your search."
: "No servers available."}
</div>
) : (
<div className="cards-grid">
{servers
.sort((a, b) => {
// Sort built-in servers first
if (a.is_builtin && !b.is_builtin) return -1;
if (!a.is_builtin && b.is_builtin) return 1;
return 0;
})
.map((server) => (
<motion.div
key={server.id}
initial={{
opacity: 0,
}}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6 }}
>
<ServerCard key={server.id} server={server} />
</motion.div>
))}
</div>
)}
</section>
</div>
</Layout>
);
}
@@ -0,0 +1,133 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Regular.woff2)
format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Medium.woff2)
format("woff2");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Semibold.woff2)
format("woff2");
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: Cash Sans;
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Bold.woff2)
format("woff2");
font-weight: 700;
font-style: normal;
}
@layer base {
:root {
/* start arcade colors */
--constant-white: #ffffff;
--constant-black: #000000;
--grey-10: #101010;
--grey-20: #1e1e1e;
--grey-50: #666666;
--grey-60: #959595;
--grey-80: #cccccc;
--grey-85: #dadada;
--grey-90: #e8e8e8;
--grey-95: #f0f0f0;
--dark-grey-15: #1a1a1a;
--dark-grey-25: #232323;
--dark-grey-30: #2a2a2a;
--dark-grey-40: #333333;
--dark-grey-45: #595959;
--dark-grey-60: #878787;
--dark-grey-90: #e1e1e1;
--background-app: var(--constant-white);
--background-prominent: var(--grey-80);
--background-standard: var(--grey-90);
--background-subtle: var(--grey-95);
--border-divider: var(--grey-90);
--border-inverse: var(--constant-white);
--border-prominent: var(--grey-10);
--border-standard: var(--grey-60);
--border-subtle: var(--grey-90);
--icon-disabled: var(--grey-60);
--icon-extra-subtle: var(--grey-60);
--icon-inverse: var(--constant-white);
--icon-prominent: var(--grey-10);
--icon-standard: var(--grey-20);
--icon-subtle: var(--grey-50);
--text-placeholder: var(--grey-60);
--text-prominent: var(--grey-10);
--text-standard: var(--grey-20);
--text-subtle: var(--grey-50);
&.dark {
--background-app: var(--constant-black);
--background-prominent: var(--dark-grey-40);
--background-standard: var(--dark-grey-25);
--background-subtle: var(--dark-grey-15);
--border-divider: var(--dark-grey-25);
--border-inverse: var(--constant-black);
--border-prominent: var(--constant-white);
--border-standard: var(--dark-grey-45);
--border-subtle: var(--dark-grey-25);
--icon-disabled: var(--dark-grey-45);
--icon-extra-subtle: var(--dark-grey-45);
--icon-inverse: var(--constant-black);
--icon-prominent: var(--constant-white);
--icon-standard: var(--dark-grey-90);
--icon-subtle: var(--dark-grey-60);
--text-placeholder: var(--dark-grey-45);
--text-prominent: var(--constant-white);
--text-standard: var(--dark-grey-90);
--text-subtle: var(--dark-grey-60);
}
/* end arcade colors */
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes appear-top {
0% {
transform: translateY(-50%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* view transitions */
a.transitioning .home-page-server-name,
.detail-page-server-name {
view-transition-name: server-name;
}