Files
Arcrun/landing/app/integrations/page.tsx
T
uncle6me-web 5d00e71275 chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)
頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:13:33 +08:00

165 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const runtime = 'edge';
import Link from 'next/link';
import SiteNav from '../components/SiteNav';
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]">
<SiteNav currentPath="/integrations" />
<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>
);
}