636 lines
18 KiB
TypeScript
636 lines
18 KiB
TypeScript
import React from "react";
|
|
import {
|
|
AbsoluteFill,
|
|
Img,
|
|
interpolate,
|
|
spring,
|
|
useCurrentFrame,
|
|
useVideoConfig,
|
|
Series,
|
|
staticFile,
|
|
} from "remotion";
|
|
|
|
// ============================================
|
|
// SPRING CONFIGS (from best practices)
|
|
// ============================================
|
|
const SPRING_SMOOTH = { damping: 200 };
|
|
const SPRING_SNAPPY = { damping: 20, stiffness: 200 };
|
|
const SPRING_BOUNCY = { damping: 12 };
|
|
|
|
// ============================================
|
|
// CAMERA - Simple wrapper, ONE motion at a time
|
|
// ============================================
|
|
const Camera: React.FC<{
|
|
children: React.ReactNode;
|
|
zoom?: number;
|
|
x?: number;
|
|
y?: number;
|
|
}> = ({ children, zoom = 1, x = 0, y = 0 }) => (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
transform: `scale(${zoom}) translate(${x}px, ${y}px)`,
|
|
transformOrigin: "center center",
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
// ============================================
|
|
// KINETIC TEXT - Word by word reveal
|
|
// ============================================
|
|
const KineticText: React.FC<{
|
|
text: string;
|
|
delay?: number;
|
|
stagger?: number;
|
|
style?: React.CSSProperties;
|
|
}> = ({ text, delay = 0, stagger = 4, style = {} }) => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
const words = text.split(" ");
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.3em", ...style }}>
|
|
{words.map((word, i) => {
|
|
const wordDelay = delay + i * stagger;
|
|
const progress = spring({
|
|
frame: frame - wordDelay,
|
|
fps,
|
|
config: SPRING_SNAPPY,
|
|
});
|
|
|
|
return (
|
|
<span
|
|
key={i}
|
|
style={{
|
|
display: "inline-block",
|
|
transform: `translateY(${interpolate(progress, [0, 1], [40, 0])}px)`,
|
|
opacity: progress,
|
|
}}
|
|
>
|
|
{word}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// BROWSER MOCKUP
|
|
// ============================================
|
|
const BrowserMockup: React.FC<{
|
|
src: string;
|
|
width?: number;
|
|
}> = ({ src, width = 1000 }) => {
|
|
const height = width * 0.5625;
|
|
return (
|
|
<div
|
|
style={{
|
|
backgroundColor: "#1e293b",
|
|
borderRadius: 12,
|
|
overflow: "hidden",
|
|
boxShadow: "0 25px 60px rgba(0,0,0,0.4)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 36,
|
|
backgroundColor: "#0f172a",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "0 14px",
|
|
gap: 7,
|
|
}}
|
|
>
|
|
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#ef4444" }} />
|
|
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#f59e0b" }} />
|
|
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#22c55e" }} />
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
marginLeft: 14,
|
|
height: 22,
|
|
backgroundColor: "#1e293b",
|
|
borderRadius: 5,
|
|
paddingLeft: 10,
|
|
fontSize: 11,
|
|
color: "#64748b",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
app.reonomy.com
|
|
</div>
|
|
</div>
|
|
<Img src={src} style={{ width, height, objectFit: "cover" }} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// STEP LABEL
|
|
// ============================================
|
|
const StepLabel: React.FC<{
|
|
step: number;
|
|
text: string;
|
|
color?: string;
|
|
}> = ({ step, text, color = "#6366f1" }) => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const progress = spring({ frame: frame - 10, fps, config: SPRING_SNAPPY });
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 50,
|
|
left: 50,
|
|
transform: `translateY(${interpolate(progress, [0, 1], [30, 0])}px)`,
|
|
opacity: progress,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background: `linear-gradient(135deg, ${color}, ${color}dd)`,
|
|
padding: "14px 28px",
|
|
borderRadius: 10,
|
|
boxShadow: `0 10px 30px ${color}40`,
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 20, fontWeight: 700, color: "white" }}>
|
|
Step {step}: {text}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// CONTACT CARD
|
|
// ============================================
|
|
const ContactCard: React.FC<{
|
|
type: "phone" | "email";
|
|
value: string;
|
|
source?: string;
|
|
delay: number;
|
|
}> = ({ type, value, source, delay }) => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const progress = spring({ frame: frame - delay, fps, config: SPRING_SNAPPY });
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 14,
|
|
padding: "12px 16px",
|
|
backgroundColor: "rgba(255,255,255,0.95)",
|
|
borderRadius: 10,
|
|
marginBottom: 8,
|
|
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
|
|
transform: `translateY(${interpolate(progress, [0, 1], [20, 0])}px)`,
|
|
opacity: progress,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 8,
|
|
background:
|
|
type === "phone"
|
|
? "linear-gradient(135deg, #10b981, #059669)"
|
|
: "linear-gradient(135deg, #6366f1, #4f46e5)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 18,
|
|
}}
|
|
>
|
|
{type === "phone" ? "📞" : "📧"}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 14, fontWeight: 600, color: "#1e293b" }}>{value}</div>
|
|
{source && <div style={{ fontSize: 11, color: "#64748b" }}>{source}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// ANIMATED STAT
|
|
// ============================================
|
|
const AnimatedStat: React.FC<{
|
|
value: number | string;
|
|
label: string;
|
|
color: string;
|
|
delay: number;
|
|
}> = ({ value, label, color, delay }) => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const progress = spring({ frame: frame - delay, fps, config: SPRING_SMOOTH });
|
|
const numericValue =
|
|
typeof value === "number" ? Math.round(interpolate(progress, [0, 1], [0, value])) : value;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
textAlign: "center",
|
|
transform: `scale(${interpolate(progress, [0, 1], [0.8, 1])})`,
|
|
opacity: progress,
|
|
}}
|
|
>
|
|
<div style={{ fontSize: 48, fontWeight: 800, color }}>{numericValue}</div>
|
|
<div style={{ fontSize: 14, color: "#94a3b8", marginTop: 4 }}>{label}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// SCENES
|
|
// ============================================
|
|
|
|
const SceneIntro: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
|
|
// Slow, elegant zoom settle: 1.12 -> 1.0 over longer duration
|
|
const zoom = interpolate(frame, [0, 120], [1.12, 1.0], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<div style={{ textAlign: "center" }}>
|
|
<KineticText
|
|
text="🏢 Reonomy Contact Extractor"
|
|
delay={20}
|
|
stagger={6}
|
|
style={{ fontSize: 64, fontWeight: 800, color: "white", justifyContent: "center" }}
|
|
/>
|
|
<div style={{ marginTop: 20 }}>
|
|
<KineticText
|
|
text="CRE Leads → Your CRM … On Demand"
|
|
delay={70}
|
|
stagger={5}
|
|
style={{ fontSize: 28, fontWeight: 500, color: "#94a3b8", justifyContent: "center" }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Camera>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneLogin: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
|
|
// Slow push in toward login area: 0.92 -> 1.08 over 140 frames
|
|
const zoom = interpolate(frame, [0, 140], [0.92, 1.08], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<BrowserMockup src={staticFile("assets/02-login-filled.png")} width={1000} />
|
|
</Camera>
|
|
<StepLabel step={1} text="Login" />
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneSearch: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
// Slower settle: start wide, ease in over 100 frames
|
|
const zoom = interpolate(frame, [0, 100], [0.88, 1.0], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
const filterProgress1 = spring({ frame: frame - 50, fps, config: SPRING_SNAPPY });
|
|
const filterProgress2 = spring({ frame: frame - 70, fps, config: SPRING_SNAPPY });
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<div style={{ position: "relative" }}>
|
|
<BrowserMockup src={staticFile("assets/04-search-results.png")} width={1050} />
|
|
|
|
{/* Filter badges - staggered */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
left: -160,
|
|
top: 140,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background: "rgba(16, 185, 129, 0.9)",
|
|
padding: "10px 18px",
|
|
borderRadius: 20,
|
|
marginBottom: 10,
|
|
boxShadow: "0 4px 20px rgba(16, 185, 129, 0.3)",
|
|
opacity: filterProgress1,
|
|
transform: `translateX(${interpolate(filterProgress1, [0, 1], [-30, 0])}px)`,
|
|
}}
|
|
>
|
|
<span style={{ color: "white", fontWeight: 600, fontSize: 14 }}>✓ Has Phone</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
background: "rgba(99, 102, 241, 0.9)",
|
|
padding: "10px 18px",
|
|
borderRadius: 20,
|
|
boxShadow: "0 4px 20px rgba(99, 102, 241, 0.3)",
|
|
opacity: filterProgress2,
|
|
transform: `translateX(${interpolate(filterProgress2, [0, 1], [-30, 0])}px)`,
|
|
}}
|
|
>
|
|
<span style={{ color: "white", fontWeight: 600, fontSize: 14 }}>✓ Has Email</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Camera>
|
|
<StepLabel step={2} text="Filtered Search" />
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneProperty: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
|
|
// Slow, cinematic pan down over 120 frames
|
|
const y = interpolate(frame, [0, 120], [-25, 15], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera y={y}>
|
|
<BrowserMockup src={staticFile("assets/05-property-detail.png")} width={1000} />
|
|
</Camera>
|
|
<StepLabel step={3} text="Property Details" />
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneOwner: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
|
|
// Slow zoom in to owner section over 120 frames
|
|
const zoom = interpolate(frame, [0, 120], [1.0, 1.12], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<BrowserMockup src={staticFile("assets/06-owner-tab.png")} width={1000} />
|
|
</Camera>
|
|
<StepLabel step={4} text="Owner Tab" color="#8b5cf6" />
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneModal: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
// Slower pop in then settle over longer duration
|
|
const zoom = interpolate(frame, [0, 50, 100], [0.85, 1.04, 1.0], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
const labelProgress = spring({ frame: frame - 70, fps, config: SPRING_BOUNCY });
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<BrowserMockup src={staticFile("assets/09-contact-modal.png")} width={1050} />
|
|
</Camera>
|
|
|
|
{/* Success label */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 50,
|
|
left: "50%",
|
|
transform: `translateX(-50%) translateY(${interpolate(labelProgress, [0, 1], [40, 0])}px)`,
|
|
opacity: labelProgress,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background: "linear-gradient(135deg, #10b981, #059669)",
|
|
padding: "16px 40px",
|
|
borderRadius: 12,
|
|
boxShadow: "0 15px 40px rgba(16, 185, 129, 0.4)",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 24, fontWeight: 700, color: "white" }}>
|
|
🎉 Contacts Extracted!
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
const SceneResults: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
// Slow, elegant zoom out over 150 frames
|
|
const zoom = interpolate(frame, [0, 150], [1.06, 1.0], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
const phones = [
|
|
{ value: "919-469-9553", source: "Greystone Property Mgmt" },
|
|
{ value: "727-341-0186", source: "Seaside Villas" },
|
|
{ value: "903-566-9506", source: "Apartment Income Reit" },
|
|
{ value: "407-671-2400", source: "E R Management" },
|
|
{ value: "407-382-2683", source: "Bellagio Apartments" },
|
|
];
|
|
const emails = [
|
|
{ value: "berrizoro@gmail.com" },
|
|
{ value: "aberriz@hotmail.com" },
|
|
{ value: "jasonhitch1@gmail.com" },
|
|
{ value: "albert@annarborusa.org" },
|
|
{ value: "albertb@sterlinghousing.com" },
|
|
];
|
|
|
|
const headerProgress = spring({ frame: frame - 10, fps, config: SPRING_SMOOTH });
|
|
const phoneHeaderProgress = spring({ frame: frame - 60, fps, config: SPRING_SMOOTH });
|
|
const emailHeaderProgress = spring({ frame: frame - 70, fps, config: SPRING_SMOOTH });
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Camera zoom={zoom}>
|
|
<div style={{ width: 1600, padding: 40 }}>
|
|
{/* Title */}
|
|
<div style={{ textAlign: "center", marginBottom: 30 }}>
|
|
<div
|
|
style={{
|
|
opacity: headerProgress,
|
|
transform: `translateY(${interpolate(headerProgress, [0, 1], [20, 0])}px)`,
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 42, fontWeight: 800, color: "white" }}>
|
|
CRE Leads Delivered Direct to Your CRM
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: 10,
|
|
opacity: spring({ frame: frame - 25, fps, config: SPRING_SMOOTH }),
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
fontWeight: 600,
|
|
background: "linear-gradient(90deg, #6366f1, #a855f7, #ec4899)",
|
|
WebkitBackgroundClip: "text",
|
|
WebkitTextFillColor: "transparent",
|
|
}}
|
|
>
|
|
… On Demand 🚀
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contact columns */}
|
|
<div style={{ display: "flex", gap: 40, paddingLeft: 80, paddingRight: 80 }}>
|
|
<div style={{ flex: 1 }}>
|
|
<div
|
|
style={{
|
|
fontSize: 18,
|
|
color: "#10b981",
|
|
marginBottom: 12,
|
|
fontWeight: 700,
|
|
opacity: phoneHeaderProgress,
|
|
}}
|
|
>
|
|
📞 Phone Numbers
|
|
</div>
|
|
{phones.map((p, i) => (
|
|
<ContactCard
|
|
key={p.value}
|
|
type="phone"
|
|
value={p.value}
|
|
source={p.source}
|
|
delay={80 + i * 15}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ flex: 1 }}>
|
|
<div
|
|
style={{
|
|
fontSize: 18,
|
|
color: "#6366f1",
|
|
marginBottom: 12,
|
|
fontWeight: 700,
|
|
opacity: emailHeaderProgress,
|
|
}}
|
|
>
|
|
📧 Email Addresses
|
|
</div>
|
|
{emails.map((e, i) => (
|
|
<ContactCard key={e.value} type="email" value={e.value} delay={90 + i * 15} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats footer */}
|
|
<div
|
|
style={{
|
|
marginTop: 30,
|
|
display: "flex",
|
|
justifyContent: "space-around",
|
|
background: "rgba(255,255,255,0.05)",
|
|
backdropFilter: "blur(10px)",
|
|
padding: "20px 50px",
|
|
borderRadius: 14,
|
|
marginLeft: 80,
|
|
marginRight: 80,
|
|
}}
|
|
>
|
|
<AnimatedStat value={10} label="Total Contacts" color="#6366f1" delay={200} />
|
|
<AnimatedStat value={5} label="Phone Numbers" color="#10b981" delay={220} />
|
|
<AnimatedStat value={5} label="Emails" color="#f59e0b" delay={240} />
|
|
<AnimatedStat value="~60s" label="Extraction Time" color="#ef4444" delay={260} />
|
|
</div>
|
|
</div>
|
|
</Camera>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// ============================================
|
|
// MAIN COMPOSITION
|
|
// ============================================
|
|
export const ReonomyDemo: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { durationInFrames } = useVideoConfig();
|
|
|
|
// Global fade in/out (20 frames as recommended)
|
|
const fadeIn = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" });
|
|
const fadeOut = interpolate(frame, [durationInFrames - 20, durationInFrames], [1, 0], {
|
|
extrapolateLeft: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
background: "linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%)",
|
|
opacity: Math.min(fadeIn, fadeOut),
|
|
}}
|
|
>
|
|
<Series>
|
|
<Series.Sequence durationInFrames={210}>
|
|
<SceneIntro />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={180}>
|
|
<SceneLogin />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={210}>
|
|
<SceneSearch />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={180}>
|
|
<SceneProperty />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={180}>
|
|
<SceneOwner />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={210}>
|
|
<SceneModal />
|
|
</Series.Sequence>
|
|
|
|
<Series.Sequence durationInFrames={420}>
|
|
<SceneResults />
|
|
</Series.Sequence>
|
|
</Series>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|