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:
Trey t
2026-03-23 22:21:45 -05:00
parent 66c2bbec8b
commit 80a1ffbe4d
29 changed files with 1279 additions and 78 deletions
+77 -6
View File
@@ -9,6 +9,10 @@ import {
TrendingUp,
Calendar,
Settings,
ChevronsUpDown,
Check,
Plus,
AppWindow,
} from "lucide-react";
import {
Sidebar,
@@ -21,6 +25,14 @@ import {
SidebarMenuItem,
SidebarHeader,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useActiveApp } from "@/hooks/use-active-app";
const navItems = [
{ title: "Dashboard", href: "/", icon: LayoutDashboard },
@@ -28,21 +40,80 @@ const navItems = [
{ title: "Assets", href: "/assets", icon: Image },
{ title: "Trends", href: "/trends", icon: TrendingUp },
{ title: "Queue", href: "/queue", icon: Calendar },
{ title: "Apps", href: "/apps", icon: AppWindow },
{ title: "Settings", href: "/settings", icon: Settings },
];
export function AppSidebar() {
const pathname = usePathname();
const { apps, activeApp, setActiveApp } = useActiveApp();
return (
<Sidebar>
<SidebarHeader className="border-b px-6 py-4">
<Link href="/" className="flex items-center gap-2 font-semibold">
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary">
<Megaphone className="h-4 w-4 text-primary-foreground" />
</div>
<span>honeyDue Marketing</span>
</Link>
<DropdownMenu>
<DropdownMenuTrigger
render={
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left font-semibold hover:bg-accent/50 focus:outline-none" />
}
>
{activeApp ? (
<img
src={`/api/files/apps/${activeApp.slug}/icon.png`}
alt={activeApp.name}
className="h-7 w-7 rounded-md object-cover"
onError={(e) => {
const el = e.target as HTMLImageElement;
el.style.display = "none";
el.nextElementSibling?.classList.remove("hidden");
}}
/>
) : null}
<div
className={`flex h-7 w-7 items-center justify-center rounded-md ${activeApp ? "hidden" : ""}`}
style={{ backgroundColor: activeApp?.primaryColor || "hsl(var(--primary))" }}
>
<Megaphone className="h-4 w-4 text-white" />
</div>
<span className="flex-1 truncate">
{activeApp?.name || "Select App"}
</span>
<ChevronsUpDown className="h-4 w-4 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="w-56">
{apps.map((app) => (
<DropdownMenuItem
key={app.slug}
onClick={() => setActiveApp(app.slug)}
className="flex items-center gap-2"
>
<img
src={`/api/files/apps/${app.slug}/icon.png`}
alt={app.name}
className="h-5 w-5 rounded object-cover"
onError={(e) => {
const el = e.target as HTMLImageElement;
el.style.display = "none";
if (el.nextElementSibling) el.nextElementSibling.classList.remove("hidden");
}}
/>
<div
className="hidden h-5 w-5 rounded"
style={{ backgroundColor: app.primaryColor }}
/>
<span className="flex-1">{app.name}</span>
{activeApp?.slug === app.slug && (
<Check className="h-4 w-4" />
)}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem render={<Link href="/apps/new" />} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<span>Add App</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>