4516cdee4b
- landing/: Next.js 15 app for arcrun.dev (dashboard, integrations, API docs, login). Deploys via Cloudflare Pages — CI scan skips this via pages_build_output_dir marker. - builtins/: minimal Hono Worker at arcrun-builtins (/init for one-shot component registry seeding). initComponents logic is flagged stale in src/index.ts for future rewrite. - BETA_TEST.md: pre-launch validation playbook. - README.md: updated to match current arcrun.dev / acr CLI flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
176 lines
8.4 KiB
TypeScript
176 lines
8.4 KiB
TypeScript
export const runtime = 'edge';
|
||
|
||
import Link from 'next/link';
|
||
|
||
type Recipe = {
|
||
id: string;
|
||
name: string;
|
||
primitive: 'static_key' | 'service_account';
|
||
category: string;
|
||
secrets: string[];
|
||
badge?: 'official';
|
||
};
|
||
|
||
const RECIPES: Recipe[] = [
|
||
// AI / LLM
|
||
{ id: 'openai', name: 'OpenAI', primitive: 'static_key', category: 'AI', secrets: ['OPENAI_API_KEY'], badge: 'official' },
|
||
{ id: 'anthropic', name: 'Anthropic', primitive: 'static_key', category: 'AI', secrets: ['ANTHROPIC_API_KEY'], badge: 'official' },
|
||
// Productivity
|
||
{ id: 'notion', name: 'Notion', primitive: 'static_key', category: 'Productivity', secrets: ['NOTION_TOKEN'], badge: 'official' },
|
||
{ id: 'airtable', name: 'Airtable', primitive: 'static_key', category: 'Productivity', secrets: ['AIRTABLE_TOKEN'], badge: 'official' },
|
||
{ id: 'typeform', name: 'Typeform', primitive: 'static_key', category: 'Productivity', secrets: ['TYPEFORM_TOKEN'], badge: 'official' },
|
||
{ id: 'jira', name: 'Jira', primitive: 'static_key', category: 'Productivity', secrets: ['JIRA_DOMAIN', 'JIRA_EMAIL', 'JIRA_API_TOKEN'], badge: 'official' },
|
||
// Communication
|
||
{ id: 'slack', name: 'Slack', primitive: 'static_key', category: 'Communication', secrets: ['SLACK_TOKEN'], badge: 'official' },
|
||
{ id: 'discord', name: 'Discord', primitive: 'static_key', category: 'Communication', secrets: ['DISCORD_BOT_TOKEN'], badge: 'official' },
|
||
{ id: 'twilio', name: 'Twilio', primitive: 'static_key', category: 'Communication', secrets: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'], badge: 'official' },
|
||
{ id: 'sendgrid', name: 'SendGrid', primitive: 'static_key', category: 'Communication', secrets: ['SENDGRID_API_KEY'], badge: 'official' },
|
||
{ id: 'resend', name: 'Resend', primitive: 'static_key', category: 'Communication', secrets: ['RESEND_API_KEY'], badge: 'official' },
|
||
// Dev / Code
|
||
{ id: 'github', name: 'GitHub', primitive: 'static_key', category: 'Dev', secrets: ['GITHUB_TOKEN'], badge: 'official' },
|
||
{ id: 'linear', name: 'Linear', primitive: 'static_key', category: 'Dev', secrets: ['LINEAR_API_KEY'], badge: 'official' },
|
||
{ id: 'supabase', name: 'Supabase', primitive: 'static_key', category: 'Dev', secrets: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'], badge: 'official' },
|
||
// Commerce
|
||
{ id: 'stripe', name: 'Stripe', primitive: 'static_key', category: 'Commerce', secrets: ['STRIPE_SECRET_KEY'], badge: 'official' },
|
||
{ id: 'shopify', name: 'Shopify', primitive: 'static_key', category: 'Commerce', secrets: ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ACCESS_TOKEN'], badge: 'official' },
|
||
{ id: 'hubspot', name: 'HubSpot', primitive: 'static_key', category: 'Commerce', secrets: ['HUBSPOT_ACCESS_TOKEN'], badge: 'official' },
|
||
// Google Service Account
|
||
{ id: 'google_drive_sa', name: 'Google Drive', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
|
||
{ id: 'google_gmail_sa', name: 'Gmail', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
|
||
{ id: 'google_sheets_sa', name: 'Google Sheets', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
|
||
];
|
||
|
||
const CATEGORIES = ['All', 'AI', 'Productivity', 'Communication', 'Dev', 'Commerce', 'Google'];
|
||
|
||
export default function IntegrationsPage({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<{ cat?: string }>;
|
||
}) {
|
||
return <IntegrationsContent searchParamsPromise={searchParams} />;
|
||
}
|
||
|
||
async function IntegrationsContent({
|
||
searchParamsPromise,
|
||
}: {
|
||
searchParamsPromise: Promise<{ cat?: string }>;
|
||
}) {
|
||
const params = await searchParamsPromise;
|
||
const cat = params.cat ?? 'All';
|
||
const filtered = cat === 'All' ? RECIPES : RECIPES.filter(r => r.category === cat);
|
||
|
||
const staticCount = RECIPES.filter(r => r.primitive === 'static_key').length;
|
||
const saCount = RECIPES.filter(r => r.primitive === 'service_account').length;
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#0a0a0a] text-[#ededed]">
|
||
{/* Nav */}
|
||
<nav className="flex items-center justify-between px-6 py-4 border-b border-[#1a1a1a]">
|
||
<Link href="/" className="text-white font-bold text-lg tracking-tight hover:opacity-80 transition-opacity">
|
||
arcrun
|
||
</Link>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<Link href="/api-docs" className="text-[#666] hover:text-white transition-colors">API</Link>
|
||
<Link href="/login"
|
||
className="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-1.5 rounded-md text-sm font-medium transition-colors">
|
||
Get API Key
|
||
</Link>
|
||
</div>
|
||
</nav>
|
||
|
||
<div className="max-w-5xl mx-auto px-6 py-12">
|
||
{/* Header */}
|
||
<h1 className="text-3xl font-bold text-white mb-2">
|
||
{RECIPES.length} 個已驗證的認證服務
|
||
</h1>
|
||
<p className="text-[#555] mb-2">
|
||
由 arcrun 團隊維護,每個 recipe 都通過整合測試。
|
||
</p>
|
||
<div className="flex gap-4 text-sm text-[#444] mb-8">
|
||
<span>{staticCount} API Key 類</span>
|
||
<span>·</span>
|
||
<span>{saCount} Service Account 類</span>
|
||
</div>
|
||
|
||
{/* Category filter */}
|
||
<div className="flex gap-2 flex-wrap mb-8">
|
||
{CATEGORIES.map(c => (
|
||
<Link
|
||
key={c}
|
||
href={c === 'All' ? '/integrations' : `/integrations?cat=${c}`}
|
||
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
|
||
cat === c
|
||
? 'bg-indigo-600 text-white'
|
||
: 'bg-[#111] border border-[#222] text-[#666] hover:text-white hover:border-[#444]'
|
||
}`}
|
||
>
|
||
{c}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
|
||
{/* Recipe grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-12">
|
||
{filtered.map(recipe => (
|
||
<RecipeCard key={recipe.id} recipe={recipe} />
|
||
))}
|
||
</div>
|
||
|
||
{/* Contribute CTA */}
|
||
<div className="bg-[#111] border border-[#222] rounded-2xl p-8 text-center">
|
||
<h2 className="text-white font-semibold text-xl mb-2">找不到你要的服務?</h2>
|
||
<p className="text-[#555] text-sm mb-4 max-w-lg mx-auto">
|
||
大部分 API Key 類的服務,填一份 YAML 就能加進來。
|
||
把 API 文件丟給 AI,五分鐘生成,開 PR 送出。
|
||
</p>
|
||
<div className="flex gap-3 justify-center flex-wrap">
|
||
<a href="https://github.com/richblack/arcrun" target="_blank" rel="noopener noreferrer"
|
||
className="bg-indigo-600 hover:bg-indigo-500 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors">
|
||
開始貢獻
|
||
</a>
|
||
<a href="https://github.com/richblack/arcrun/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer"
|
||
className="border border-[#333] hover:border-[#555] text-[#aaa] hover:text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors">
|
||
查看 Recipe 格式
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||
const primitiveLabel = recipe.primitive === 'static_key' ? 'API Key' : 'Service Account';
|
||
const primitiveColor = recipe.primitive === 'static_key' ? 'text-blue-400' : 'text-orange-400';
|
||
|
||
return (
|
||
<div className="bg-[#111] border border-[#1e1e1e] hover:border-[#333] rounded-xl p-5 transition-colors">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div>
|
||
<h3 className="text-white font-medium">{recipe.name}</h3>
|
||
<span className={`text-xs font-mono mt-0.5 ${primitiveColor}`}>{primitiveLabel}</span>
|
||
</div>
|
||
{recipe.badge === 'official' && (
|
||
<span className="text-xs bg-indigo-950/50 text-indigo-400 border border-indigo-900/30 px-2 py-0.5 rounded-full">
|
||
★ 官方
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-xs text-[#444] space-y-1">
|
||
{recipe.secrets.map(s => (
|
||
<div key={s} className="font-mono flex items-center gap-1">
|
||
<span className="text-[#333]">›</span> {s}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-4 text-xs">
|
||
<code className="text-[#555] bg-[#0a0a0a] px-2 py-1 rounded font-mono">
|
||
acr auth-recipe scaffold {recipe.id}
|
||
</code>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|