From 32bac40b0bfab8d424b98f3eba648598c9118125 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Jun 2026 22:01:32 +0000 Subject: [PATCH] feat(dashboard): add LaunchPix API platform dashboard --- app/dashboard/(api)/api/apps/page.tsx | 26 +++ app/dashboard/(api)/api/billing/page.tsx | 11 ++ app/dashboard/(api)/api/keys/page.tsx | 17 ++ app/dashboard/(api)/api/layout.tsx | 21 +++ app/dashboard/(api)/api/page.tsx | 95 ++++++++++ app/dashboard/(api)/api/usage/page.tsx | 14 ++ app/dashboard/{ => (workspace)}/layout.tsx | 0 app/dashboard/{ => (workspace)}/page.tsx | 0 .../projects/[id]/assets/page.tsx | 0 .../projects/[id]/generate/page.tsx | 0 .../{ => (workspace)}/projects/[id]/page.tsx | 0 .../{ => (workspace)}/projects/new/page.tsx | 0 .../{ => (workspace)}/projects/page.tsx | 0 app/docs/api/page.tsx | 7 +- app/globals.css | 8 + components/api-dashboard/api-alert-banner.tsx | 48 ++++++ .../api-dashboard/api-dashboard-shell.tsx | 28 +++ components/api-dashboard/api-docs-snippet.tsx | 21 +++ components/api-dashboard/api-keys-table.tsx | 48 ++++++ components/api-dashboard/api-page-header.tsx | 21 +++ components/api-dashboard/api-sidebar.tsx | 162 ++++++++++++++++++ components/api-dashboard/billing-tabs.tsx | 69 ++++++++ .../api-dashboard/create-key-button.tsx | 14 ++ components/api-dashboard/empty-state.tsx | 19 ++ components/api-dashboard/get-started-card.tsx | 27 +++ components/api-dashboard/metric-card.tsx | 19 ++ components/api-dashboard/types.ts | 4 + .../api-dashboard/usage-chart-placeholder.tsx | 44 +++++ components/dashboard/sidebar.tsx | 4 +- lib/api-dashboard/greeting.ts | 12 ++ lib/api-dashboard/mock-data.ts | 22 +++ next.config.ts | 7 + 32 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 app/dashboard/(api)/api/apps/page.tsx create mode 100644 app/dashboard/(api)/api/billing/page.tsx create mode 100644 app/dashboard/(api)/api/keys/page.tsx create mode 100644 app/dashboard/(api)/api/layout.tsx create mode 100644 app/dashboard/(api)/api/page.tsx create mode 100644 app/dashboard/(api)/api/usage/page.tsx rename app/dashboard/{ => (workspace)}/layout.tsx (100%) rename app/dashboard/{ => (workspace)}/page.tsx (100%) rename app/dashboard/{ => (workspace)}/projects/[id]/assets/page.tsx (100%) rename app/dashboard/{ => (workspace)}/projects/[id]/generate/page.tsx (100%) rename app/dashboard/{ => (workspace)}/projects/[id]/page.tsx (100%) rename app/dashboard/{ => (workspace)}/projects/new/page.tsx (100%) rename app/dashboard/{ => (workspace)}/projects/page.tsx (100%) create mode 100644 components/api-dashboard/api-alert-banner.tsx create mode 100644 components/api-dashboard/api-dashboard-shell.tsx create mode 100644 components/api-dashboard/api-docs-snippet.tsx create mode 100644 components/api-dashboard/api-keys-table.tsx create mode 100644 components/api-dashboard/api-page-header.tsx create mode 100644 components/api-dashboard/api-sidebar.tsx create mode 100644 components/api-dashboard/billing-tabs.tsx create mode 100644 components/api-dashboard/create-key-button.tsx create mode 100644 components/api-dashboard/empty-state.tsx create mode 100644 components/api-dashboard/get-started-card.tsx create mode 100644 components/api-dashboard/metric-card.tsx create mode 100644 components/api-dashboard/types.ts create mode 100644 components/api-dashboard/usage-chart-placeholder.tsx create mode 100644 lib/api-dashboard/greeting.ts create mode 100644 lib/api-dashboard/mock-data.ts diff --git a/app/dashboard/(api)/api/apps/page.tsx b/app/dashboard/(api)/api/apps/page.tsx new file mode 100644 index 0000000..dba8c3a --- /dev/null +++ b/app/dashboard/(api)/api/apps/page.tsx @@ -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 ( +
+ + + View documentation + + } + /> +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/(api)/api/billing/page.tsx b/app/dashboard/(api)/api/billing/page.tsx new file mode 100644 index 0000000..af3c274 --- /dev/null +++ b/app/dashboard/(api)/api/billing/page.tsx @@ -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 ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/(api)/api/keys/page.tsx b/app/dashboard/(api)/api/keys/page.tsx new file mode 100644 index 0000000..d10ca9a --- /dev/null +++ b/app/dashboard/(api)/api/keys/page.tsx @@ -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 ( +
+ } + /> + +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/(api)/api/layout.tsx b/app/dashboard/(api)/api/layout.tsx new file mode 100644 index 0000000..d1983fe --- /dev/null +++ b/app/dashboard/(api)/api/layout.tsx @@ -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 ( + + {children} + + ); +} \ No newline at end of file diff --git a/app/dashboard/(api)/api/page.tsx b/app/dashboard/(api)/api/page.tsx new file mode 100644 index 0000000..a741a1a --- /dev/null +++ b/app/dashboard/(api)/api/page.tsx @@ -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; + + return ( +
+
+

+ {greeting} + {name ? `, ${name}` : ""} +

+

+ Generate launch images, banners, social creatives, and product launch packs from one API. +

+
+ +
+ + + + +
+ +
+

Get started

+
+ } + /> + } + /> + } + /> +
+
+ +
+

Build with agents

+
+
+
+ + + +
+

MCP server

+

+ Connect LaunchPix to your preferred agent workflow for automated launch asset generation. +

+ + Read integration guide + +
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/(api)/api/usage/page.tsx b/app/dashboard/(api)/api/usage/page.tsx new file mode 100644 index 0000000..f2662dd --- /dev/null +++ b/app/dashboard/(api)/api/usage/page.tsx @@ -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 ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/layout.tsx b/app/dashboard/(workspace)/layout.tsx similarity index 100% rename from app/dashboard/layout.tsx rename to app/dashboard/(workspace)/layout.tsx diff --git a/app/dashboard/page.tsx b/app/dashboard/(workspace)/page.tsx similarity index 100% rename from app/dashboard/page.tsx rename to app/dashboard/(workspace)/page.tsx diff --git a/app/dashboard/projects/[id]/assets/page.tsx b/app/dashboard/(workspace)/projects/[id]/assets/page.tsx similarity index 100% rename from app/dashboard/projects/[id]/assets/page.tsx rename to app/dashboard/(workspace)/projects/[id]/assets/page.tsx diff --git a/app/dashboard/projects/[id]/generate/page.tsx b/app/dashboard/(workspace)/projects/[id]/generate/page.tsx similarity index 100% rename from app/dashboard/projects/[id]/generate/page.tsx rename to app/dashboard/(workspace)/projects/[id]/generate/page.tsx diff --git a/app/dashboard/projects/[id]/page.tsx b/app/dashboard/(workspace)/projects/[id]/page.tsx similarity index 100% rename from app/dashboard/projects/[id]/page.tsx rename to app/dashboard/(workspace)/projects/[id]/page.tsx diff --git a/app/dashboard/projects/new/page.tsx b/app/dashboard/(workspace)/projects/new/page.tsx similarity index 100% rename from app/dashboard/projects/new/page.tsx rename to app/dashboard/(workspace)/projects/new/page.tsx diff --git a/app/dashboard/projects/page.tsx b/app/dashboard/(workspace)/projects/page.tsx similarity index 100% rename from app/dashboard/projects/page.tsx rename to app/dashboard/(workspace)/projects/page.tsx diff --git a/app/docs/api/page.tsx b/app/docs/api/page.tsx index 7029c39..18bc827 100644 --- a/app/docs/api/page.tsx +++ b/app/docs/api/page.tsx @@ -20,9 +20,14 @@ export default function ApiDocsPage() {
+ + + + {mobileOpen ? ( +
+
+ ) : null} + + + +
+ + ); +} \ No newline at end of file diff --git a/components/api-dashboard/billing-tabs.tsx b/components/api-dashboard/billing-tabs.tsx new file mode 100644 index 0000000..4bca792 --- /dev/null +++ b/components/api-dashboard/billing-tabs.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import { EmptyState } from "@/components/api-dashboard/empty-state"; + +const tabs = ["Billing Information", "Credits", "Invoices"] as const; + +type TabId = (typeof tabs)[number]; + +export function BillingTabs() { + const [active, setActive] = useState("Billing Information"); + + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {active === "Billing Information" ? ( +
+

+ Add a payment method to start using the LaunchPix API. Your account begins with an initial refill after + billing is configured. +

+ + Add payment method + + } + /> +
+ ) : null} + + {active === "Credits" ? ( + + ) : null} + + {active === "Invoices" ? ( + + ) : null} +
+
+ ); +} \ No newline at end of file diff --git a/components/api-dashboard/create-key-button.tsx b/components/api-dashboard/create-key-button.tsx new file mode 100644 index 0000000..17e51c6 --- /dev/null +++ b/components/api-dashboard/create-key-button.tsx @@ -0,0 +1,14 @@ +"use client"; + +export function CreateKeyButton() { + return ( + + ); +} \ No newline at end of file diff --git a/components/api-dashboard/empty-state.tsx b/components/api-dashboard/empty-state.tsx new file mode 100644 index 0000000..ece0ad9 --- /dev/null +++ b/components/api-dashboard/empty-state.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +export function EmptyState({ + title, + description, + action +}: { + title: string; + description?: string; + action?: ReactNode; +}) { + return ( +
+

{title}

+ {description ?

{description}

: null} + {action ?
{action}
: null} +
+ ); +} \ No newline at end of file diff --git a/components/api-dashboard/get-started-card.tsx b/components/api-dashboard/get-started-card.tsx new file mode 100644 index 0000000..c3c5b5c --- /dev/null +++ b/components/api-dashboard/get-started-card.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; + +export function GetStartedCard({ + title, + description, + href, + icon +}: { + title: string; + description: string; + href: string; + icon: ReactNode; +}) { + return ( + + + {icon} + +

{title}

+

{description}

+ + ); +} \ No newline at end of file diff --git a/components/api-dashboard/metric-card.tsx b/components/api-dashboard/metric-card.tsx new file mode 100644 index 0000000..f506b04 --- /dev/null +++ b/components/api-dashboard/metric-card.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +export function MetricCard({ + label, + value, + hint +}: { + label: string; + value: string; + hint?: ReactNode; +}) { + return ( +
+

{label}

+

{value}

+ {hint ?

{hint}

: null} +
+ ); +} \ No newline at end of file diff --git a/components/api-dashboard/types.ts b/components/api-dashboard/types.ts new file mode 100644 index 0000000..f54b88a --- /dev/null +++ b/components/api-dashboard/types.ts @@ -0,0 +1,4 @@ +export type ApiSetupState = { + hasPaymentMethod: boolean; + hasApiKey: boolean; +}; \ No newline at end of file diff --git a/components/api-dashboard/usage-chart-placeholder.tsx b/components/api-dashboard/usage-chart-placeholder.tsx new file mode 100644 index 0000000..8240487 --- /dev/null +++ b/components/api-dashboard/usage-chart-placeholder.tsx @@ -0,0 +1,44 @@ +const yLabels = ["$0.20", "$0.10", "$0"]; +const xLabels = ["May 28", "May 29", "May 30", "May 31", "Jun 1", "Jun 2", "Jun 3"]; + +export function UsageChartPlaceholder() { + return ( +
+
+
+ {yLabels.map((label) => ( +
+ + {label} + +
+ ))} +
+ + + + + + +
+ {xLabels.map((label) => ( + + {label} + + ))} + Last 7 days +
+
+

Usage chart preview. Connect billing meters to populate live generation spend.

+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx index a241257..ac06249 100644 --- a/components/dashboard/sidebar.tsx +++ b/components/dashboard/sidebar.tsx @@ -3,13 +3,14 @@ import Link from "next/link"; import { signOut } from "next-auth/react"; import { usePathname } from "next/navigation"; -import { ChevronUp, CreditCard, Folder, Gem, Home, ImageIcon, LogOut, Menu, Plus, Settings, UserCircle, Wand2, X } from "lucide-react"; +import { ChevronUp, Code2, CreditCard, Folder, Gem, Home, ImageIcon, LogOut, Menu, Plus, Settings, UserCircle, Wand2, X } from "lucide-react"; import { useEffect, useState } from "react"; import { LaunchPixLogo } from "@/components/brand/logo"; import { cn } from "@/lib/utils"; const navItems = [ { href: "/dashboard", label: "Overview", icon: Home, section: "overview" }, + { href: "/dashboard/api", label: "API Platform", icon: Code2, section: "api-platform" }, { href: "/dashboard/projects", label: "Projects", icon: Folder, section: "projects" }, { href: "/dashboard/projects/new?step=3", label: "Generations", icon: Wand2, section: "generations" }, { href: "/dashboard/projects", label: "Assets", icon: ImageIcon, section: "assets" }, @@ -27,6 +28,7 @@ function getInitials(email: string) { function isActive(pathname: string, section: (typeof navItems)[number]["section"]) { if (section === "overview") return pathname === "/dashboard"; + if (section === "api-platform") return pathname.startsWith("/dashboard/api"); if (section === "projects") return pathname.startsWith("/dashboard/projects") && !pathname.includes("/dashboard/projects/new") && !pathname.endsWith("/assets") && !pathname.endsWith("/generate"); if (section === "generations") return pathname.endsWith("/generate") || pathname.includes("/dashboard/projects/new"); if (section === "assets") return pathname.endsWith("/assets"); diff --git a/lib/api-dashboard/greeting.ts b/lib/api-dashboard/greeting.ts new file mode 100644 index 0000000..742fb48 --- /dev/null +++ b/lib/api-dashboard/greeting.ts @@ -0,0 +1,12 @@ +export function getTimeGreeting(date = new Date()) { + const hour = date.getHours(); + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; +} + +export function getDisplayName(name?: string | null, email?: string | null) { + if (name?.trim()) return name.trim().split(" ")[0]; + if (email?.trim()) return email.split("@")[0]; + return null; +} \ No newline at end of file diff --git a/lib/api-dashboard/mock-data.ts b/lib/api-dashboard/mock-data.ts new file mode 100644 index 0000000..40d8f80 --- /dev/null +++ b/lib/api-dashboard/mock-data.ts @@ -0,0 +1,22 @@ +/** UI-only placeholders until per-user API keys and billing meters ship. */ + +export type ApiKeyRow = { + id: string; + partialKey: string; + createdAt: string; + status: "active" | "revoked"; +}; + +export const MOCK_API_KEYS: ApiKeyRow[] = []; + +export const MOCK_METRICS = { + availableCreditsUsd: "$0.00", + totalSpentUsd: "$0.00", + totalIssuedUsd: "$0.00", + activeApiKeys: 0 +}; + +export const MOCK_SETUP = { + hasPaymentMethod: false, + hasApiKey: false +}; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index a3c7dba..5f5da44 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,13 @@ const nextConfig: NextConfig = { }, images: { remotePatterns: [{ protocol: "https", hostname: "**" }] + }, + async redirects() { + return [ + { source: "/dashboard/api-keys", destination: "/dashboard/api/keys", permanent: false }, + { source: "/dashboard/usage", destination: "/dashboard/api/usage", permanent: false }, + { source: "/api", destination: "/dashboard/api", permanent: false } + ]; } };