Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/dashboard/(api)/api/apps/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from "next/link";
import { ApiPageHeader } from "@/components/api-dashboard/api-page-header";
import { EmptyState } from "@/components/api-dashboard/empty-state";

export default function ApiAppsPage() {
return (
<div>
<ApiPageHeader
title="Apps"
subtitle="Connect LaunchPix to agents, apps, and launch workflows."
/>
<EmptyState
title="No connected apps yet"
description="Register apps to scope API keys and monitor usage by integration."
action={
<Link
href="/docs/api"
className="inline-flex rounded-lg bg-[#f5f5f5] px-4 py-2 text-sm font-medium text-[#050505] hover:opacity-90"
>
View documentation
</Link>
}
/>
</div>
);
}
11 changes: 11 additions & 0 deletions app/dashboard/(api)/api/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiPageHeader } from "@/components/api-dashboard/api-page-header";
import { BillingTabs } from "@/components/api-dashboard/billing-tabs";

export default function ApiBillingPage() {
return (
<div>
<ApiPageHeader title="Billing" />
<BillingTabs />
</div>
);
}
17 changes: 17 additions & 0 deletions app/dashboard/(api)/api/keys/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiKeysTable } from "@/components/api-dashboard/api-keys-table";
import { ApiPageHeader } from "@/components/api-dashboard/api-page-header";
import { CreateKeyButton } from "@/components/api-dashboard/create-key-button";
import { MOCK_API_KEYS } from "@/lib/api-dashboard/mock-data";

export default function ApiKeysPage() {
return (
<div>
<ApiPageHeader
title="API Keys"
subtitle="API keys are used to authenticate requests to the LaunchPix API."
action={<CreateKeyButton />}
/>
<ApiKeysTable keys={MOCK_API_KEYS} />
</div>
);
}
21 changes: 21 additions & 0 deletions app/dashboard/(api)/api/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ReactNode } from "react";
import { ApiDashboardShell } from "@/components/api-dashboard/api-dashboard-shell";
import { MOCK_API_KEYS, MOCK_SETUP } from "@/lib/api-dashboard/mock-data";
import { requireUser } from "@/lib/supabase/auth";

export const dynamic = "force-dynamic";

export default async function ApiPlatformLayout({ children }: { children: ReactNode }) {
const { user } = await requireUser();

const setup = {
hasPaymentMethod: MOCK_SETUP.hasPaymentMethod,
hasApiKey: MOCK_SETUP.hasApiKey || MOCK_API_KEYS.length > 0
};

return (
<ApiDashboardShell setup={setup} userEmail={user.email ?? "developer@launchpix.dev"}>
{children}
</ApiDashboardShell>
);
}
95 changes: 95 additions & 0 deletions app/dashboard/(api)/api/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Link from "next/link";
import { BookOpen, Bot, KeyRound, LineChart } from "lucide-react";
import { ApiDocsSnippet } from "@/components/api-dashboard/api-docs-snippet";
import { GetStartedCard } from "@/components/api-dashboard/get-started-card";
import { MetricCard } from "@/components/api-dashboard/metric-card";
import { MOCK_METRICS } from "@/lib/api-dashboard/mock-data";
import { getDisplayName, getTimeGreeting } from "@/lib/api-dashboard/greeting";
import { requireUser } from "@/lib/supabase/auth";
import { getAccessContext } from "@/lib/services/access/permissions";

export default async function ApiPlatformHomePage() {
const { user } = await requireUser();
const { subscription } = await getAccessContext(user.id);
const greeting = getTimeGreeting();
const name = getDisplayName(user.name, user.email);
const creditsDisplay =
subscription.credits_remaining > 0
? `${subscription.credits_remaining} credits`
: MOCK_METRICS.availableCreditsUsd;
Comment on lines +17 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show zero credits as credits, not dollars

When a user has exhausted their balance (subscription.credits_remaining === 0), this fallback makes the “Available credits” card display "$0.00" instead of "0 credits", mixing currency with the credit balance while the rest of the app treats credits_remaining as the source of truth. Use the subscription value for zero as well so the dashboard accurately reports the user's credit state.

Useful? React with 👍 / 👎.


return (
<div className="space-y-8">
<header>
<h1 className="text-2xl font-semibold tracking-tight text-[#f5f5f5] sm:text-[1.65rem]">
{greeting}
{name ? `, ${name}` : ""}
</h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[#8a8a8a]">
Generate launch images, banners, social creatives, and product launch packs from one API.
</p>
</header>

<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard label="Available credits" value={creditsDisplay} hint="Track generation usage and credits." />
<MetricCard label="Total spent" value={MOCK_METRICS.totalSpentUsd} />
<MetricCard label="Total issued" value={MOCK_METRICS.totalIssuedUsd} />
<MetricCard
label="Active API keys"
value={String(MOCK_METRICS.activeApiKeys)}
hint={MOCK_METRICS.activeApiKeys === 0 ? "0 active keys" : undefined}
/>
</section>

<section>
<h2 className="text-sm font-medium text-[#f5f5f5]">Get started</h2>
<div className="mt-4 grid gap-4 sm:grid-cols-3">
<GetStartedCard
title="Create an API key"
description="API keys authenticate requests to LaunchPix."
href="/dashboard/api/keys"
icon={<KeyRound className="size-4" />}
/>
<GetStartedCard
title="View docs"
description="Launch-ready visuals through one API."
href="/docs/api"
icon={<BookOpen className="size-4" />}
/>
<GetStartedCard
title="Track usage"
description="Monitor generation spend and credit consumption."
href="/dashboard/api/usage"
icon={<LineChart className="size-4" />}
/>
</div>
</section>

<section>
<h2 className="text-sm font-medium text-[#f5f5f5]">Build with agents</h2>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
<div className="api-dashboard-card rounded-[14px] border border-[rgba(255,255,255,0.08)] bg-[#111111] p-5 sm:p-6">
<div className="flex items-start gap-3">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#0d0d0d]">
<Bot className="size-4 text-[#a1a1a1]" />
</span>
<div>
<p className="text-sm font-medium text-[#f5f5f5]">MCP server</p>
<p className="mt-1.5 text-sm leading-6 text-[#8a8a8a]">
Connect LaunchPix to your preferred agent workflow for automated launch asset generation.
</p>
<Link
href="/docs/api"
className="mt-3 inline-block text-sm text-[#a1a1a1] underline-offset-2 hover:text-[#f5f5f5] hover:underline"
>
Read integration guide
</Link>
</div>
</div>
</div>
<ApiDocsSnippet />
</div>
</section>
</div>
);
}
14 changes: 14 additions & 0 deletions app/dashboard/(api)/api/usage/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiPageHeader } from "@/components/api-dashboard/api-page-header";
import { UsageChartPlaceholder } from "@/components/api-dashboard/usage-chart-placeholder";

export default function ApiUsagePage() {
return (
<div>
<ApiPageHeader
title="Usage"
subtitle="View and track your API usage over time."
/>
<UsageChartPlaceholder />
</div>
);
}
File renamed without changes.
File renamed without changes.
7 changes: 6 additions & 1 deletion app/docs/api/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export default function ApiDocsPage() {

<div className="mt-8 flex flex-wrap gap-3">
<Button asChild>
<Link href="/dashboard/api">
Open API dashboard
<ArrowRight className="size-4" />
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/contact">
Request API key
<ArrowRight className="size-4" />
</Link>
</Button>
<Button asChild variant="outline">
Expand Down
8 changes: 8 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,12 @@ body {
.dashboard-label {
@apply text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground;
}

.api-dashboard {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.api-dashboard-card {
@apply shadow-none;
}
}
48 changes: 48 additions & 0 deletions components/api-dashboard/api-alert-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link from "next/link";
import { AlertTriangle } from "lucide-react";

export function ApiAlertBanner({
hasPaymentMethod,
hasApiKey
}: {
hasPaymentMethod: boolean;
hasApiKey: boolean;
}) {
const messages: string[] = [];
if (!hasPaymentMethod) messages.push("Add a payment method to enable API billing and credit refills.");
if (!hasApiKey) messages.push("Create an API key to authenticate LaunchPix API requests.");

return (
<div
role="status"
className="border-b border-[rgba(220,80,80,0.25)] bg-[#3b0d0d] px-4 py-3 text-sm text-[#e8a0a0] sm:px-6"
>
<div className="mx-auto flex max-w-[960px] flex-wrap items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-[#c96a6a]" aria-hidden />
<div className="min-w-0 flex-1 space-y-1">
{messages.map((message) => (
<p key={message}>{message}</p>
))}
</div>
<div className="flex flex-wrap gap-2">
{!hasApiKey ? (
<Link
href="/dashboard/api/keys"
className="rounded-lg border border-[rgba(255,255,255,0.12)] bg-[rgba(255,255,255,0.06)] px-3 py-1.5 text-xs font-medium text-[#f5f5f5] hover:bg-[rgba(255,255,255,0.1)]"
>
Create API key
</Link>
) : null}
{!hasPaymentMethod ? (
<Link
href="/dashboard/api/billing"
className="rounded-lg border border-[rgba(255,255,255,0.12)] bg-[rgba(255,255,255,0.06)] px-3 py-1.5 text-xs font-medium text-[#f5f5f5] hover:bg-[rgba(255,255,255,0.1)]"
>
Add payment method
</Link>
) : null}
</div>
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions components/api-dashboard/api-dashboard-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ReactNode } from "react";
import { ApiAlertBanner } from "@/components/api-dashboard/api-alert-banner";
import { ApiSidebar } from "@/components/api-dashboard/api-sidebar";
import type { ApiSetupState } from "@/components/api-dashboard/types";

export function ApiDashboardShell({
children,
setup,
userEmail
}: {
children: ReactNode;
setup: ApiSetupState;
userEmail: string;
}) {
const showAlert = !setup.hasPaymentMethod || !setup.hasApiKey;

return (
<div className="api-dashboard min-h-screen bg-[#050505] text-[#f5f5f5]">
<ApiSidebar userEmail={userEmail} />
<div className="api-dashboard-main flex min-h-screen min-w-0 flex-1 flex-col lg:pl-[240px]">
{showAlert ? <ApiAlertBanner hasPaymentMethod={setup.hasPaymentMethod} hasApiKey={setup.hasApiKey} /> : null}
<main className="flex-1 px-4 py-6 sm:px-6 sm:py-8">
<div className="mx-auto w-full max-w-[960px]">{children}</div>
</main>
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions components/api-dashboard/api-docs-snippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function ApiDocsSnippet() {
return (
<div className="api-dashboard-card rounded-[14px] border border-[rgba(255,255,255,0.08)] bg-[#111111] p-5 sm:p-6">
<p className="text-xs font-medium uppercase tracking-[0.14em] text-[#8a8a8a]">Quick example</p>
<pre className="mt-4 overflow-x-auto rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#0a0a0a] p-4 font-mono text-xs leading-6 text-[#d4d4d4]">
<code>{`POST /v1/launch-pack

{
"productName": "TeraAI",
"description": "AI learning companion for students and builders",
"launchGoal": "announce product launch",
"style": "clean startup launch graphic"
}`}</code>
</pre>
<p className="mt-3 text-xs leading-5 text-[#8a8a8a]">
Docs-only example. Production routes use{" "}
<span className="font-mono text-[#a1a1a1]">/api/v1/projects</span> and generation endpoints today.
</p>
</div>
);
}
48 changes: 48 additions & 0 deletions components/api-dashboard/api-keys-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ApiKeyRow } from "@/lib/api-dashboard/mock-data";
import { EmptyState } from "@/components/api-dashboard/empty-state";

export function ApiKeysTable({ keys }: { keys: ApiKeyRow[] }) {
if (!keys.length) {
return (
<EmptyState
title="No API key yet"
description="Create a key to authenticate requests to LaunchPix. Keys are shown partially for security."
/>
);
}

return (
<div className="api-dashboard-card overflow-hidden rounded-[14px] border border-[rgba(255,255,255,0.08)] bg-[#111111]">
<div className="overflow-x-auto">
<table className="w-full min-w-[480px] text-left text-sm">
<thead>
<tr className="border-b border-[rgba(255,255,255,0.08)] text-xs text-[#8a8a8a]">
<th className="px-5 py-3 font-medium">Partial key</th>
<th className="px-5 py-3 font-medium">Date created</th>
<th className="px-5 py-3 font-medium">Status</th>
</tr>
</thead>
<tbody>
{keys.map((key) => (
<tr key={key.id} className="border-b border-[rgba(255,255,255,0.06)] last:border-0">
<td className="px-5 py-4 font-mono text-[#f5f5f5]">{key.partialKey}</td>
<td className="px-5 py-4 text-[#a1a1a1]">{key.createdAt}</td>
<td className="px-5 py-4">
<span
className={
key.status === "active"
? "text-[#9ed49e]"
: "text-[#8a8a8a]"
}
>
{key.status === "active" ? "Active" : "Revoked"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Loading