Files
Sportstime/marketing-videos/src/videos/TheFanTest/RoadGamesScene.tsx
Trey t 8e937a5646 feat: fix travel placement bug, add theme-based alternate icons, fix animated background crash
- Fix repeat-city travel placement: use stop indices instead of global city name
  matching so Follow Team trips with repeat cities show travel correctly
- Add TravelPlacement helper and regression tests (7 tests)
- Add alternate app icons for each theme, auto-switch on theme change
- Fix index-out-of-range crash in AnimatedSportsBackground (19 configs, was iterating 20)
- Add marketing video configs, engine, and new video components
- Add docs and data exports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 09:36:34 -06:00

322 lines
8.0 KiB
TypeScript

import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "../../components/shared/theme";
import { AppScreenshot, MockScreen } from "../../components/shared/AppScreenshot";
/**
* Scene 3: Road games surfaced
*
* Shows upcoming away games inside a phone frame, cards sliding in.
* On-screen text: "Plan it in seconds"
*/
type GameCard = {
opponent: string;
opponentColor: string;
date: string;
venue: string;
city: string;
};
const ROAD_GAMES: GameCard[] = [
{
opponent: "@ Dodgers",
opponentColor: "#005A9C",
date: "Fri, Jun 12",
venue: "Dodger Stadium",
city: "Los Angeles, CA",
},
{
opponent: "@ Giants",
opponentColor: "#FD5A1E",
date: "Sun, Jun 14",
venue: "Oracle Park",
city: "San Francisco, CA",
},
{
opponent: "@ Padres",
opponentColor: "#2F241D",
date: "Tue, Jun 16",
venue: "Petco Park",
city: "San Diego, CA",
},
{
opponent: "@ D-backs",
opponentColor: "#A71930",
date: "Thu, Jun 18",
venue: "Chase Field",
city: "Phoenix, AZ",
},
];
export const RoadGamesScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// "Plan it in seconds" label overlay (outside phone)
const planLabelProgress = spring({
frame: frame - 2 * fps,
fps,
config: theme.animation.snappy,
});
const planLabelOpacity = interpolate(
frame - 2 * fps,
[0, 0.2 * fps],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
background: theme.colors.background,
justifyContent: "center",
alignItems: "center",
}}
>
{/* Phone frame with app UI */}
<AppScreenshot delay={0} scale={0.88}>
<MockScreen>
<RoadGamesScreenContent />
</MockScreen>
</AppScreenshot>
{/* "Plan it in seconds" label - overlaid outside phone */}
<div
style={{
position: "absolute",
bottom: 120,
left: 0,
right: 0,
display: "flex",
justifyContent: "center",
opacity: planLabelOpacity,
transform: `scale(${interpolate(planLabelProgress, [0, 1], [0.8, 1])})`,
zIndex: 10,
}}
>
<div
style={{
background: theme.colors.accent,
padding: "14px 36px",
borderRadius: 40,
boxShadow: "0 8px 24px rgba(255, 107, 53, 0.4)",
}}
>
<span
style={{
fontFamily: theme.fonts.display,
fontSize: 30,
fontWeight: 700,
color: theme.colors.text,
}}
>
Plan it in seconds
</span>
</div>
</div>
</AbsoluteFill>
);
};
/** Inner screen content rendered inside the phone frame */
const RoadGamesScreenContent: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Header entrance
const headerProgress = spring({
frame,
fps,
config: theme.animation.smooth,
});
const headerOpacity = interpolate(frame, [0, 0.3 * fps], [0, 1], {
extrapolateRight: "clamp",
});
const headerY = interpolate(headerProgress, [0, 1], [20, 0]);
return (
<div style={{ padding: 12 }}>
{/* Header */}
<div
style={{
opacity: headerOpacity,
transform: `translateY(${headerY}px)`,
marginBottom: 32,
}}
>
{/* Team badge */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 12,
marginBottom: 8,
}}
>
<div
style={{
width: 40,
height: 40,
borderRadius: 10,
background: "#C9082A",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<span
style={{
fontFamily: theme.fonts.display,
fontSize: 16,
fontWeight: 900,
color: "white",
}}
>
ATL
</span>
</div>
<div
style={{
fontFamily: theme.fonts.display,
fontSize: 28,
fontWeight: 700,
color: theme.colors.text,
}}
>
Braves Road Games
</div>
</div>
<div
style={{
fontFamily: theme.fonts.text,
fontSize: 18,
color: theme.colors.textSecondary,
}}
>
June 2026 away stretch
</div>
</div>
{/* Game cards */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 14,
}}
>
{ROAD_GAMES.map((game, index) => {
const cardDelay = 0.3 + index * 0.2;
const cardProgress = spring({
frame: frame - cardDelay * fps,
fps,
config: theme.animation.snappy,
});
const translateX = interpolate(cardProgress, [0, 1], [300, 0]);
const opacity = interpolate(cardProgress, [0, 1], [0, 1]);
return (
<div
key={game.venue}
style={{
background: "#1C1C1E",
borderRadius: 16,
padding: 22,
display: "flex",
alignItems: "center",
gap: 16,
transform: `translateX(${translateX}px)`,
opacity,
borderLeft: `4px solid ${game.opponentColor}`,
}}
>
{/* Date block */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
minWidth: 60,
}}
>
<div
style={{
fontFamily: theme.fonts.text,
fontSize: 13,
color: theme.colors.textMuted,
textTransform: "uppercase",
letterSpacing: 1,
}}
>
{game.date.split(", ")[0]}
</div>
<div
style={{
fontFamily: theme.fonts.display,
fontSize: 22,
fontWeight: 700,
color: theme.colors.text,
}}
>
{game.date.split(" ")[1].replace(",", "")}
</div>
</div>
{/* Divider */}
<div
style={{
width: 2,
height: 40,
background: "rgba(255,255,255,0.1)",
borderRadius: 1,
}}
/>
{/* Game info */}
<div style={{ flex: 1 }}>
<div
style={{
fontFamily: theme.fonts.display,
fontSize: 20,
fontWeight: 700,
color: theme.colors.text,
marginBottom: 3,
}}
>
{game.opponent}
</div>
<div
style={{
fontFamily: theme.fonts.text,
fontSize: 15,
color: theme.colors.textSecondary,
}}
>
{game.venue}
</div>
<div
style={{
fontFamily: theme.fonts.text,
fontSize: 13,
color: theme.colors.textMuted,
}}
>
{game.city}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};