feat: add landing page + builtins Worker + BETA_TEST guide + README
- 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>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user