feat: complete marketing command center with pipeline, UI, and asset generation

- Dashboard with campaign management, asset gallery, and publishing queue
- 7-agent pipeline: trend scout, research, scripts, ad creative, video, copy, distribution
- Campaign form with screenshot upload, goal picker, platform selection
- Campaign detail view with Details/Pipeline/Assets/Chat tabs
- Two-set image generation: Gemini AI (NanoBanana MCP) + Canvas Design posters
- Remotion video rendering with phone.png frame and real screenshot alignment
- honeyDue branding: blue #0079FF, orange #FF9400, Inter font, warm off-white
- Asset cards with source badges (Gemini/Canvas/Remotion/Playwright)
- Markdown/JSON render endpoint for viewing pipeline outputs as HTML
- Settings page with Tavily, Gemini, Postiz, Nextdoor integration management
- Claude Chat for campaign feedback loop with streaming SSE
- Postiz publishing modal with scheduling
- Auth with NextAuth credentials + JWT sessions
- SQLite via Prisma with better-sqlite3 adapter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-23 21:05:26 -05:00
parent 6b08cfb73a
commit 66c2bbec8b
113 changed files with 12741 additions and 138 deletions
+121
View File
@@ -0,0 +1,121 @@
import { getSetting } from "./settings";
const NEXTDOOR_API_URL = "https://ads.nextdoor.com/v1";
async function getNextdoorConfig() {
const token = await getSetting("NEXTDOOR_API_TOKEN");
const advertiserId = await getSetting("NEXTDOOR_ADVERTISER_ID");
return { token, advertiserId };
}
async function nextdoorFetch(query: string, variables: Record<string, unknown> = {}) {
const { token } = await getNextdoorConfig();
const res = await fetch(`${NEXTDOOR_API_URL}/graphql`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Nextdoor API error ${res.status}: ${text}`);
}
const data = await res.json();
if (data.errors?.length) {
throw new Error(`Nextdoor GraphQL error: ${data.errors[0].message}`);
}
return data.data;
}
export async function createNextdoorCampaign(
name: string,
budget: number,
schedule: { startDate: string; endDate: string }
) {
const mutation = `
mutation CreateCampaign($input: CreateCampaignInput!) {
createCampaign(input: $input) {
campaign { id name status }
}
}
`;
return nextdoorFetch(mutation, {
input: {
advertiserId: (await getNextdoorConfig()).advertiserId,
name,
objective: "WEBSITE_CONVERSION",
budget: { amount: budget, currency: "USD" },
schedule,
},
});
}
export async function createNextdoorAdGroup(
campaignId: string,
targeting: Record<string, unknown>
) {
const mutation = `
mutation CreateAdGroup($input: CreateAdGroupInput!) {
createAdGroup(input: $input) {
adGroup { id name status }
}
}
`;
return nextdoorFetch(mutation, {
input: {
campaignId,
name: "Auto-generated Ad Group",
targeting,
},
});
}
export async function uploadNextdoorCreative(imageUrl: string) {
const mutation = `
mutation CreateCreative($input: CreateCreativeInput!) {
createCreative(input: $input) {
creative { id status }
}
}
`;
return nextdoorFetch(mutation, {
input: {
advertiserId: (await getNextdoorConfig()).advertiserId,
imageUrl,
type: "IMAGE",
},
});
}
export async function createNextdoorAd(
adGroupId: string,
creativeId: string,
copy: { headline: string; body: string; ctaText: string; destinationUrl: string }
) {
const mutation = `
mutation CreateAd($input: CreateAdInput!) {
createAd(input: $input) {
ad { id name status }
}
}
`;
return nextdoorFetch(mutation, {
input: {
adGroupId,
creativeId,
headline: copy.headline,
body: copy.body,
callToAction: copy.ctaText,
destinationUrl: copy.destinationUrl,
},
});
}