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:
+121
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user