66c2bbec8b
- 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>
122 lines
2.8 KiB
TypeScript
122 lines
2.8 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
}
|