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>
This commit is contained in:
321
marketing-videos/src/videos/TheFanTest/RoadGamesScene.tsx
Normal file
321
marketing-videos/src/videos/TheFanTest/RoadGamesScene.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user