feat: add multi-app support with app switcher, per-app branding, and filtered queries
Apps share the same backend, API keys, and publishing flow but each gets its own branding (name, colors, icon, app URL), knowledge files (brand identity, product info, platform guidelines), and campaigns. The pipeline dynamically writes _knowledge/ files and copies app assets before each run. - Add App model with slug, colors, appUrl, and knowledge markdown fields - Add appId FK to Campaign, seed honeyDue as first app with existing knowledge - App switcher dropdown in sidebar with icon previews - Filter campaigns, stats, and assets by active app (cookie-based) - De-hardcode lib/claude.ts: AppConfig interface, templated prompts, dynamic _knowledge/ and Remotion asset copying - App management pages (list, create, edit) with icon upload and color pickers - Asset library sort options (newest, oldest, name, platform, type) - Asset cards show creation date - Remotion HoneyDueAd accepts colors/appName props Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
interface AppItem {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
primaryColor: string;
|
||||
accentColor: string;
|
||||
createdAt: string;
|
||||
_count: { campaigns: number };
|
||||
}
|
||||
|
||||
export default function AppsPage() {
|
||||
const [apps, setApps] = useState<AppItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/apps")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
setApps(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Apps</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage apps sharing this marketing pipeline
|
||||
</p>
|
||||
</div>
|
||||
<Button render={<Link href="/apps/new" />}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add App
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
) : apps.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-10 text-center text-muted-foreground">
|
||||
No apps yet. Create one to get started.
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{apps.map((app) => (
|
||||
<Link key={app.id} href={`/apps/${app.slug}`}>
|
||||
<Card className="transition-shadow hover:shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg"
|
||||
style={{ backgroundColor: app.primaryColor }}
|
||||
/>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{app.name}</CardTitle>
|
||||
<CardDescription>/{app.slug}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
|
||||
{app.description || "No description"}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="h-3 w-3 rounded"
|
||||
style={{ backgroundColor: app.primaryColor }}
|
||||
/>
|
||||
<span>{app.primaryColor}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="h-3 w-3 rounded"
|
||||
style={{ backgroundColor: app.accentColor }}
|
||||
/>
|
||||
<span>{app.accentColor}</span>
|
||||
</div>
|
||||
<span className="ml-auto text-muted-foreground">
|
||||
{app._count.campaigns} campaign{app._count.campaigns !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user