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