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
+318
View File
@@ -0,0 +1,318 @@
import {
AbsoluteFill,
Img,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
staticFile,
} from "remotion";
export interface HoneyDueAdProps {
platform: "instagram" | "tiktok";
hookText: string;
bodyText: string;
ctaText: string;
proofText: string;
screenshotSrc: string;
}
const COLORS = {
primary: "#0079FF",
accent: "#FF9400",
dark: "#1a1a2e",
light: "#f8f6f2",
white: "#ffffff",
red: "#FF3B30",
};
export const HoneyDueAd: React.FC<HoneyDueAdProps> = ({
platform,
hookText,
bodyText,
ctaText,
proofText,
screenshotSrc,
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
const isPolished = platform === "instagram";
const bg = isPolished ? COLORS.dark : "#0d0d0d";
// Scene boundaries
const hookEnd = Math.floor(durationInFrames * 0.22);
const phoneStart = hookEnd;
const phoneEnd = Math.floor(durationInFrames * 0.6);
const proofStart = phoneEnd;
const proofEnd = Math.floor(durationInFrames * 0.78);
const ctaStart = proofEnd;
// === HOOK SCENE ===
const hookOpacity = interpolate(
frame,
[0, 10, hookEnd - 8, hookEnd],
[0, 1, 1, 0],
{ extrapolateRight: "clamp" }
);
const hookY = interpolate(frame, [0, 15], [40, 0], {
extrapolateRight: "clamp",
});
// === PHONE SCENE ===
const phoneScale = spring({
frame: Math.max(0, frame - phoneStart),
fps,
config: { damping: 14, stiffness: 80 },
});
const phoneOpacity = interpolate(
frame,
[phoneStart, phoneStart + 10, phoneEnd - 8, phoneEnd],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const bodyTextOpacity = interpolate(
frame,
[phoneStart + 20, phoneStart + 35],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
// === PROOF SCENE ===
const proofOpacity = interpolate(
frame,
[proofStart, proofStart + 12, proofEnd - 8, proofEnd],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const proofScale = spring({
frame: Math.max(0, frame - proofStart),
fps,
config: { damping: 12 },
});
// === CTA SCENE ===
const ctaOpacity = interpolate(frame, [ctaStart, ctaStart + 12], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const ctaScale = spring({
frame: Math.max(0, frame - ctaStart),
fps,
config: { damping: 10, stiffness: 100 },
});
// Pulse the CTA button
const ctaPulse =
frame > ctaStart + 20
? 1 + 0.03 * Math.sin((frame - ctaStart - 20) * 0.15)
: 1;
const phoneWidth = width * 0.55;
const phoneHeight = phoneWidth * 2.05;
return (
<AbsoluteFill
style={{
backgroundColor: bg,
fontFamily:
'-apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif',
}}
>
{/* Subtle gradient overlay */}
<div
style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse at 50% 30%, ${COLORS.primary}15 0%, transparent 60%)`,
}}
/>
{/* === HOOK === */}
<div
style={{
position: "absolute",
top: "38%",
left: "50%",
transform: `translate(-50%, -50%) translateY(${hookY}px)`,
opacity: hookOpacity,
width: "85%",
textAlign: "center",
}}
>
<div
style={{
fontSize: 68,
fontWeight: 800,
color: COLORS.white,
lineHeight: 1.15,
letterSpacing: -1,
}}
>
{hookText}
</div>
</div>
{/* === PHONE + BODY TEXT === */}
<div
style={{
position: "absolute",
top: "8%",
left: "50%",
transform: `translateX(-50%) scale(${phoneScale})`,
opacity: phoneOpacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 30,
}}
>
{/* Body text above phone */}
<div
style={{
opacity: bodyTextOpacity,
fontSize: 36,
fontWeight: 600,
color: COLORS.white,
textAlign: "center",
maxWidth: width * 0.8,
lineHeight: 1.3,
}}
>
{bodyText}
</div>
{/* Phone mockup — real phone.png frame over screenshot */}
<div
style={{
width: phoneWidth,
height: phoneHeight,
position: "relative",
filter: "drop-shadow(0 30px 60px rgba(0,0,0,0.5))",
}}
>
{/* Screenshot behind the frame — clipped to screen area */}
<div
style={{
position: "absolute",
top: "3.2%",
left: "4.2%",
width: "91.6%",
height: "93.6%",
borderRadius: phoneWidth * 0.065,
overflow: "hidden",
}}
>
<Img
src={screenshotSrc}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "top center",
}}
/>
</div>
{/* Phone frame on top */}
<Img
src={staticFile("phone.png")}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
</div>
{/* === PROOF === */}
<div
style={{
position: "absolute",
top: "42%",
left: "50%",
transform: `translate(-50%, -50%) scale(${proofScale})`,
opacity: proofOpacity,
textAlign: "center",
}}
>
<div
style={{
fontSize: 44,
fontWeight: 700,
color: COLORS.accent,
marginBottom: 16,
}}
>
{proofText}
</div>
<div
style={{
fontSize: 28,
fontWeight: 500,
color: "rgba(255,255,255,0.7)",
}}
>
Join thousands of homeowners
</div>
</div>
{/* === CTA === */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
opacity: ctaOpacity,
transform: `scale(${ctaScale})`,
gap: 40,
}}
>
{/* Brand name header */}
<div
style={{
fontSize: 56,
fontWeight: 800,
color: COLORS.white,
letterSpacing: -1,
textAlign: "center",
}}
>
honeyDue
</div>
{/* Icon — 50% of canvas width, centered */}
<Img
src={staticFile("icon.png")}
style={{
width: width * 0.5,
height: "auto",
borderRadius: 32,
}}
/>
{/* CTA button */}
<div
style={{
backgroundColor: COLORS.primary,
color: COLORS.white,
fontSize: 36,
fontWeight: 700,
padding: "24px 72px",
borderRadius: 20,
textAlign: "center",
boxShadow: `0 8px 32px ${COLORS.primary}80`,
transform: `scale(${ctaPulse})`,
}}
>
{ctaText}
</div>
</div>
</AbsoluteFill>
);
};