80a1ffbe4d
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>
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import {
|
|
LayoutDashboard,
|
|
Megaphone,
|
|
Image,
|
|
TrendingUp,
|
|
Calendar,
|
|
Settings,
|
|
ChevronsUpDown,
|
|
Check,
|
|
Plus,
|
|
AppWindow,
|
|
} from "lucide-react";
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
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 },
|
|
{ title: "Campaigns", href: "/campaigns", icon: Megaphone },
|
|
{ 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">
|
|
<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>
|
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{navItems.map((item) => (
|
|
<SidebarMenuItem key={item.href}>
|
|
<SidebarMenuButton
|
|
render={<Link href={item.href} />}
|
|
isActive={
|
|
item.href === "/"
|
|
? pathname === "/"
|
|
: pathname.startsWith(item.href)
|
|
}
|
|
>
|
|
<item.icon className="h-4 w-4" />
|
|
<span>{item.title}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
</Sidebar>
|
|
);
|
|
}
|