feat(remotion): mortgage broker demo video for Alex Ferrari
- Add SquareMCPBrokerDemo composition (1920x1080, 45s) - 5 scenes: title, email inbox (47 unread → 5 replies), rate blast (200 prospects), WhatsApp auto-response, Facebook page reply - Outro: SquareMCP logo + tagline - Uses existing brand colors and spring animations
This commit is contained in:
@@ -3,6 +3,7 @@ import { Composition } from "remotion";
|
|||||||
import { SquareMCPLinkedIn } from "./SquareMCPLinkedIn";
|
import { SquareMCPLinkedIn } from "./SquareMCPLinkedIn";
|
||||||
import { SquareMCPHeroLoop } from "./SquareMCPHeroLoop";
|
import { SquareMCPHeroLoop } from "./SquareMCPHeroLoop";
|
||||||
import { YCAppVideo } from "./YCAppVideo";
|
import { YCAppVideo } from "./YCAppVideo";
|
||||||
|
import { SquareMCPBrokerDemo } from "./SquareMCPBrokerDemo";
|
||||||
import {
|
import {
|
||||||
SquareMCPTikTokCTA,
|
SquareMCPTikTokCTA,
|
||||||
SquareMCPTikTokDemo,
|
SquareMCPTikTokDemo,
|
||||||
@@ -105,6 +106,14 @@ export const RemotionRoot = () => {
|
|||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
/>
|
/>
|
||||||
|
<Composition
|
||||||
|
id="SquareMCPBrokerDemo"
|
||||||
|
component={SquareMCPBrokerDemo}
|
||||||
|
durationInFrames={45 * 30}
|
||||||
|
fps={30}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
279
videos/remotion-demo/src/SquareMCPBrokerDemo.tsx
Normal file
279
videos/remotion-demo/src/SquareMCPBrokerDemo.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { COLORS, FONT, SPRING_CFG, SPRING_SOFT } from "./styles";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ─── SCENE TIMINGS ───────────────────────────────────────────────────
|
||||||
|
const SCENES = {
|
||||||
|
title: { start: 0, duration: 3 }, // 0:00–0:03
|
||||||
|
emailIn: { start: 3, duration: 10 }, // 0:03–0:13
|
||||||
|
rateBlast:{ start: 13, duration: 10 }, // 0:13–0:23
|
||||||
|
whatsapp: { start: 23, duration: 8 }, // 0:23–0:31
|
||||||
|
facebook: { start: 31, duration: 8 }, // 0:31–0:39
|
||||||
|
outro: { start: 39, duration: 6 }, // 0:39–0:45
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ─── HELPERS ─────────────────────────────────────────────────────────
|
||||||
|
function useSceneProgress(sceneStart: number, sceneDuration: number) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const startFrame = sceneStart * fps;
|
||||||
|
const endFrame = (sceneStart + sceneDuration) * fps;
|
||||||
|
const progress = interpolate(frame, [startFrame, endFrame], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
const fadeIn = interpolate(frame, [startFrame, startFrame + 0.5 * fps], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
const fadeOut = interpolate(frame, [endFrame - 0.5 * fps, endFrame], [1, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
return { progress, opacity: fadeIn * fadeOut, startFrame, endFrame };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TITLE CARD ──────────────────────────────────────────────────────
|
||||||
|
const TitleScene = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const { opacity } = useSceneProgress(SCENES.title.start, SCENES.title.duration);
|
||||||
|
|
||||||
|
const titleY = spring({ frame: frame - SCENES.title.start * fps, fps, config: SPRING_CFG, from: 40, to: 0 });
|
||||||
|
const subtitleY = spring({ frame: frame - SCENES.title.start * fps, fps, config: SPRING_CFG, from: 30, to: 0, delay: 5 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ width: 400, height: 400, borderRadius: "50%", background: `radial-gradient(circle, ${COLORS.accentGlow} 0%, transparent 70%)`, position: "absolute", top: "35%", left: "50%", transform: "translate(-50%, -50%)" }} />
|
||||||
|
<div style={{ fontSize: 64, fontWeight: 800, color: COLORS.text, letterSpacing: "-0.02em", transform: `translateY(${titleY}px)`, textShadow: `0 0 60px ${COLORS.accentGlow}` }}>
|
||||||
|
A Day in the Life
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 36, fontWeight: 700, color: COLORS.accentLight, marginTop: 16, transform: `translateY(${subtitleY}px)`, display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
|
<span style={{ color: COLORS.accent }}>■</span>
|
||||||
|
Mortgage Broker
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 20, color: COLORS.textSecondary, marginTop: 32, opacity: interpolate(frame, [1.5 * fps, 2 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) }}>
|
||||||
|
Powered by SquareMCP
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── EMAIL INBOX SCENE ───────────────────────────────────────────────
|
||||||
|
const EmailInboxScene = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const { opacity } = useSceneProgress(SCENES.emailIn.start, SCENES.emailIn.duration);
|
||||||
|
const localFrame = frame - SCENES.emailIn.start * fps;
|
||||||
|
|
||||||
|
const emails = [
|
||||||
|
{ from: "Sarah J.", subject: "Pre-approval letter?", time: "8:14 AM", unread: true },
|
||||||
|
{ from: "Mike T.", subject: "Rate lock expiration", time: "8:22 AM", unread: true },
|
||||||
|
{ from: "Linda R.", subject: "Refinance question", time: "8:31 AM", unread: true },
|
||||||
|
{ from: "David K.", subject: "Closing docs status", time: "8:45 AM", unread: true },
|
||||||
|
{ from: "Jessica M.", subject: "Down payment help", time: "9:02 AM", unread: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, padding: 60 }}>
|
||||||
|
<div style={{ fontSize: 32, fontWeight: 700, color: COLORS.text, marginBottom: 32 }}>
|
||||||
|
8:00 AM — 47 unread emails
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 40 }}>
|
||||||
|
{/* Inbox */}
|
||||||
|
<div style={{ flex: 1, background: COLORS.bgCard, borderRadius: 12, border: `1px solid ${COLORS.border}`, overflow: "hidden" }}>
|
||||||
|
{emails.map((e, i) => {
|
||||||
|
const y = spring({ frame: localFrame, fps, config: SPRING_SOFT, from: 30, to: 0, delay: i * 4 });
|
||||||
|
const op = interpolate(localFrame, [i * 4, i * 4 + 8], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ padding: "14px 20px", borderBottom: `1px solid ${COLORS.border}`, display: "flex", alignItems: "center", gap: 12, opacity: op, transform: `translateY(${y}px)` }}>
|
||||||
|
<div style={{ width: 8, height: 8, borderRadius: "50%", background: e.unread ? COLORS.accent : "transparent", flexShrink: 0 }} />
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 600, color: COLORS.text, width: 120 }}>{e.from}</div>
|
||||||
|
<div style={{ fontSize: 15, color: COLORS.textSecondary, flex: 1 }}>{e.subject}</div>
|
||||||
|
<div style={{ fontSize: 13, color: COLORS.textSecondary }}>{e.time}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* SquareMCP overlay */}
|
||||||
|
<div style={{ width: 320, opacity: interpolate(localFrame, [3 * fps, 4 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) }}>
|
||||||
|
<div style={{ background: COLORS.bgCard, borderRadius: 12, border: `1px solid ${COLORS.borderBlue}`, padding: 20 }}>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 700, color: COLORS.accentLight, marginBottom: 12 }}>■ SquareMCP</div>
|
||||||
|
<div style={{ fontSize: 14, color: COLORS.textSecondary, lineHeight: 1.6 }}>
|
||||||
|
Reading 47 emails...{"\n"}
|
||||||
|
Drafting responses...{"\n"}
|
||||||
|
<span style={{ color: COLORS.success, fontWeight: 600 }}>✓ 5 replies queued</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── RATE BLAST SCENE ────────────────────────────────────────────────
|
||||||
|
const RateBlastScene = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const { opacity } = useSceneProgress(SCENES.rateBlast.start, SCENES.rateBlast.duration);
|
||||||
|
const localFrame = frame - SCENES.rateBlast.start * fps;
|
||||||
|
|
||||||
|
const count = Math.floor(interpolate(localFrame, [2 * fps, 6 * fps], [0, 200], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }));
|
||||||
|
const barWidth = interpolate(localFrame, [2 * fps, 6 * fps], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, padding: 60, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ fontSize: 32, fontWeight: 700, color: COLORS.text, marginBottom: 40 }}>
|
||||||
|
10:30 AM — Daily Rate Update
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 600, background: COLORS.bgCard, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 32 }}>
|
||||||
|
<div style={{ fontSize: 14, color: COLORS.textSecondary, marginBottom: 8 }}>API Call</div>
|
||||||
|
<div style={{ fontSize: 16, fontFamily: "monospace", color: COLORS.accentLight, background: "rgba(14,99,246,0.1)", padding: 12, borderRadius: 6, marginBottom: 24 }}>
|
||||||
|
POST /api/email/send{"\n"}
|
||||||
|
to: 200 prospects{"\n"}
|
||||||
|
subject: "Today's Rates - May 14"{"\n"}
|
||||||
|
body: rate_table + personalized_note
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||||
|
<div style={{ flex: 1, height: 8, background: COLORS.border, borderRadius: 4, overflow: "hidden" }}>
|
||||||
|
<div style={{ width: `${barWidth}%`, height: "100%", background: COLORS.success, borderRadius: 4 }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 24, fontWeight: 800, color: COLORS.success, minWidth: 80, textAlign: "right" }}>
|
||||||
|
{count}/200
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 14, color: COLORS.textSecondary, marginTop: 12, textAlign: "center" }}>
|
||||||
|
{count >= 200 ? <span style={{ color: COLORS.success, fontWeight: 600 }}>✓ All 200 prospects notified in 3.2s</span> : "Sending..."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── WHATSAPP SCENE ──────────────────────────────────────────────────
|
||||||
|
const WhatsAppScene = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const { opacity } = useSceneProgress(SCENES.whatsapp.start, SCENES.whatsapp.duration);
|
||||||
|
const localFrame = frame - SCENES.whatsapp.start * fps;
|
||||||
|
|
||||||
|
const msgOp = interpolate(localFrame, [1 * fps, 2 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
const replyOp = interpolate(localFrame, [3 * fps, 4 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, padding: 60, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ fontSize: 32, fontWeight: 700, color: COLORS.text, marginBottom: 40 }}>
|
||||||
|
2:15 PM — Client on WhatsApp
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 420, background: COLORS.bgCard, borderRadius: 16, border: `1px solid ${COLORS.border}`, padding: 24 }}>
|
||||||
|
{/* Client message */}
|
||||||
|
<div style={{ opacity: msgOp, display: "flex", justifyContent: "flex-start", marginBottom: 16 }}>
|
||||||
|
<div style={{ background: "#2a2a3e", padding: "12px 16px", borderRadius: "18px 18px 18px 4px", maxWidth: "80%" }}>
|
||||||
|
<div style={{ fontSize: 15, color: COLORS.text }}>Hey Alex, did rates go up today?</div>
|
||||||
|
<div style={{ fontSize: 11, color: COLORS.textSecondary, marginTop: 4, textAlign: "right" }}>2:15 PM</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* SquareMCP reply */}
|
||||||
|
<div style={{ opacity: replyOp, display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
|
||||||
|
<div style={{ background: COLORS.accent, padding: "12px 16px", borderRadius: "18px 18px 4px 18px", maxWidth: "80%" }}>
|
||||||
|
<div style={{ fontSize: 15, color: "#fff" }}>Hi! Yes, 30-year fixed ticked up to 6.875%. Want me to lock you in?</div>
|
||||||
|
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.7)", marginTop: 4, textAlign: "right" }}>2:15 PM · SquareMCP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Label */}
|
||||||
|
<div style={{ opacity: replyOp, textAlign: "center", marginTop: 12 }}>
|
||||||
|
<span style={{ fontSize: 12, color: COLORS.accentLight, fontWeight: 600, background: "rgba(14,99,246,0.15)", padding: "4px 10px", borderRadius: 12 }}>
|
||||||
|
■ Auto-responded via SquareMCP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── FACEBOOK SCENE ──────────────────────────────────────────────────
|
||||||
|
const FacebookScene = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const { opacity } = useSceneProgress(SCENES.facebook.start, SCENES.facebook.duration);
|
||||||
|
const localFrame = frame - SCENES.facebook.start * fps;
|
||||||
|
|
||||||
|
const commentOp = interpolate(localFrame, [1 * fps, 2 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
const replyOp = interpolate(localFrame, [3 * fps, 4 * fps], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, padding: 60, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ fontSize: 32, fontWeight: 700, color: COLORS.text, marginBottom: 40 }}>
|
||||||
|
4:00 PM — Facebook Page
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 520, background: COLORS.bgCard, borderRadius: 12, border: `1px solid ${COLORS.border}`, padding: 24 }}>
|
||||||
|
{/* Post */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
|
||||||
|
<div style={{ width: 40, height: 40, borderRadius: "50%", background: COLORS.accent, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18, fontWeight: 800, color: "#fff" }}>F</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 700, color: COLORS.text }}>Ferrari Lending</div>
|
||||||
|
<div style={{ fontSize: 13, color: COLORS.textSecondary }}>2 hrs ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 15, color: COLORS.textSecondary, marginBottom: 20, lineHeight: 1.5 }}>
|
||||||
|
🏠 Thinking about buying your first home? DM us for a free rate quote — no credit pull required.
|
||||||
|
</div>
|
||||||
|
{/* Comment */}
|
||||||
|
<div style={{ opacity: commentOp, borderTop: `1px solid ${COLORS.border}`, paddingTop: 16 }}>
|
||||||
|
<div style={{ display: "flex", gap: 12 }}>
|
||||||
|
<div style={{ width: 32, height: 32, borderRadius: "50%", background: "#3e3e5e", flexShrink: 0 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 600, color: COLORS.text }}>Carlos V.</div>
|
||||||
|
<div style={{ fontSize: 14, color: COLORS.textSecondary, marginTop: 2 }}>Do you work with FHA loans?</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Reply */}
|
||||||
|
<div style={{ opacity: replyOp, marginTop: 12, paddingLeft: 44 }}>
|
||||||
|
<div style={{ display: "flex", gap: 12 }}>
|
||||||
|
<div style={{ width: 32, height: 32, borderRadius: "50%", background: COLORS.accent, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 800, color: "#fff" }}>F</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 600, color: COLORS.text }}>Ferrari Lending</div>
|
||||||
|
<div style={{ fontSize: 14, color: COLORS.textSecondary, marginTop: 2 }}>Yes! FHA, VA, and conventional. Send us a DM and we'll get you pre-qualified today. 🎯</div>
|
||||||
|
<div style={{ fontSize: 12, color: COLORS.accentLight, marginTop: 4 }}>■ Replied via SquareMCP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── OUTRO ───────────────────────────────────────────────────────────
|
||||||
|
const OutroScene = () => {
|
||||||
|
const { opacity } = useSceneProgress(SCENES.outro.start, SCENES.outro.duration);
|
||||||
|
const localFrame = useCurrentFrame() - SCENES.outro.start * 30;
|
||||||
|
|
||||||
|
const scale = spring({ frame: localFrame, fps: 30, config: SPRING_CFG, from: 0.9, to: 1 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ opacity, background: COLORS.bg, fontFamily: FONT, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ transform: `scale(${scale})`, display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||||
|
<div style={{ width: 64, height: 64, borderRadius: 16, background: COLORS.accent, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 28, fontWeight: 800, color: "#fff" }}>■</div>
|
||||||
|
<div style={{ fontSize: 48, fontWeight: 800, color: COLORS.text, marginTop: 24, letterSpacing: "-0.02em" }}>SquareMCP</div>
|
||||||
|
<div style={{ fontSize: 22, color: COLORS.textSecondary, marginTop: 12 }}>One API. Every Platform.</div>
|
||||||
|
<div style={{ fontSize: 16, color: COLORS.accentLight, marginTop: 32, fontWeight: 600 }}>squaremcp.com</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── MAIN COMPOSITION ────────────────────────────────────────────────
|
||||||
|
export const SquareMCPBrokerDemo = () => {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: COLORS.bg }}>
|
||||||
|
<TitleScene />
|
||||||
|
<EmailInboxScene />
|
||||||
|
<RateBlastScene />
|
||||||
|
<WhatsAppScene />
|
||||||
|
<FacebookScene />
|
||||||
|
<OutroScene />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user