chore: remove scraper, add docs, add marketing-videos gitignore

- Remove Scripts/ directory (scraper no longer needed)
- Add themed background documentation to CLAUDE.md
- Add .gitignore for marketing-videos to prevent node_modules tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-26 18:13:12 -06:00
parent bfa172de38
commit dbb0099776
129 changed files with 14805 additions and 25325 deletions

View File

@@ -0,0 +1,185 @@
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Img,
staticFile,
} from "remotion";
import { theme } from "./theme";
type AppScreenshotProps = {
src?: string;
delay?: number;
scale?: number;
showDeviceFrame?: boolean;
children?: React.ReactNode;
};
export const AppScreenshot: React.FC<AppScreenshotProps> = ({
src,
delay = 0,
scale = 0.85,
showDeviceFrame = true,
children,
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const delayFrames = delay * fps;
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.smooth,
});
const opacity = interpolate(
frame - delayFrames,
[0, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const scaleValue = interpolate(progress, [0, 1], [0.9, 1]) * scale;
if (frame < delayFrames) {
return null;
}
const phoneWidth = width * 0.75;
const phoneHeight = height * 0.8;
const cornerRadius = 60;
const bezelWidth = 12;
return (
<div
style={{
opacity,
transform: `scale(${scaleValue})`,
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
}}
>
{showDeviceFrame ? (
<div
style={{
position: "relative",
width: phoneWidth,
height: phoneHeight,
background: "#1C1C1E",
borderRadius: cornerRadius,
padding: bezelWidth,
boxShadow: `
0 50px 100px rgba(0, 0, 0, 0.5),
0 20px 40px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1)
`,
}}
>
{/* Dynamic Island */}
<div
style={{
position: "absolute",
top: bezelWidth + 15,
left: "50%",
transform: "translateX(-50%)",
width: 120,
height: 36,
background: "#000",
borderRadius: 18,
zIndex: 10,
}}
/>
{/* Screen content */}
<div
style={{
width: "100%",
height: "100%",
borderRadius: cornerRadius - bezelWidth,
overflow: "hidden",
background: theme.colors.background,
position: "relative",
}}
>
{src ? (
<Img
src={staticFile(src)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
children
)}
</div>
{/* Home indicator */}
<div
style={{
position: "absolute",
bottom: bezelWidth + 10,
left: "50%",
transform: "translateX(-50%)",
width: 140,
height: 5,
background: "rgba(255, 255, 255, 0.3)",
borderRadius: 3,
}}
/>
</div>
) : (
<div
style={{
width: phoneWidth - 40,
height: phoneHeight - 40,
borderRadius: cornerRadius - 20,
overflow: "hidden",
background: theme.colors.background,
boxShadow: `0 30px 80px rgba(0, 0, 0, 0.4)`,
}}
>
{src ? (
<Img
src={staticFile(src)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
children
)}
</div>
)}
</div>
);
};
// Mock screen content components
type MockScreenProps = {
children: React.ReactNode;
};
export const MockScreen: React.FC<MockScreenProps> = ({ children }) => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
padding: theme.spacing.lg,
paddingTop: 80, // Account for Dynamic Island
}}
>
{children}
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,79 @@
import React from "react";
import { AbsoluteFill, useCurrentFrame, interpolate } from "remotion";
import { theme } from "./theme";
type GradientBackgroundProps = {
animate?: boolean;
};
export const GradientBackground: React.FC<GradientBackgroundProps> = ({
animate = false,
}) => {
const frame = useCurrentFrame();
const gradientAngle = animate
? interpolate(frame, [0, 300], [180, 200], { extrapolateRight: "clamp" })
: 180;
return (
<AbsoluteFill
style={{
background: `linear-gradient(${gradientAngle}deg, ${theme.colors.backgroundGradientStart} 0%, ${theme.colors.backgroundGradientEnd} 100%)`,
}}
/>
);
};
// Subtle grid pattern background
export const GridBackground: React.FC = () => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
}}
>
<div
style={{
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)
`,
backgroundSize: "40px 40px",
}}
/>
</AbsoluteFill>
);
};
// Radial glow background
type GlowBackgroundProps = {
color?: string;
intensity?: number;
};
export const GlowBackground: React.FC<GlowBackgroundProps> = ({
color = theme.colors.accent,
intensity = 0.15,
}) => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
}}
>
<div
style={{
position: "absolute",
top: "30%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "150%",
height: "80%",
background: `radial-gradient(ellipse, ${color}${Math.round(intensity * 255).toString(16).padStart(2, "0")} 0%, transparent 70%)`,
}}
/>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,203 @@
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type LogoEndcardProps = {
tagline?: string;
showAppStoreBadge?: boolean;
};
export const LogoEndcard: React.FC<LogoEndcardProps> = ({
tagline,
showAppStoreBadge = false,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Logo entrance with slight bounce
const logoScale = spring({
frame,
fps,
config: { damping: 15, stiffness: 100 },
});
const logoOpacity = interpolate(frame, [0, fps * 0.3], [0, 1], {
extrapolateRight: "clamp",
});
// Tagline entrance (delayed)
const taglineProgress = spring({
frame,
fps,
delay: fps * 0.4,
config: theme.animation.smooth,
});
const taglineOpacity = interpolate(
frame,
[fps * 0.4, fps * 0.6],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const taglineY = interpolate(taglineProgress, [0, 1], [20, 0]);
// App Store badge entrance (delayed further)
const badgeProgress = spring({
frame,
fps,
delay: fps * 0.7,
config: theme.animation.smooth,
});
const badgeOpacity = interpolate(
frame,
[fps * 0.7, fps * 0.9],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
background: `linear-gradient(180deg, ${theme.colors.backgroundGradientStart} 0%, ${theme.colors.backgroundGradientEnd} 100%)`,
justifyContent: "center",
alignItems: "center",
}}
>
{/* App Icon */}
<div
style={{
transform: `scale(${logoScale})`,
opacity: logoOpacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: theme.spacing.lg,
}}
>
{/* Icon placeholder - rounded square with gradient */}
<div
style={{
width: 180,
height: 180,
borderRadius: 40,
background: `linear-gradient(135deg, ${theme.colors.accent} 0%, ${theme.colors.accentDark} 100%)`,
boxShadow: `0 20px 60px rgba(255, 107, 53, 0.4)`,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{/* Simple stadium icon representation */}
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
>
<ellipse
cx="50"
cy="60"
rx="40"
ry="20"
stroke="white"
strokeWidth="4"
fill="none"
/>
<path
d="M10 60 L10 40 Q50 10 90 40 L90 60"
stroke="white"
strokeWidth="4"
fill="none"
/>
<line
x1="50"
y1="30"
x2="50"
y2="15"
stroke="white"
strokeWidth="3"
/>
<circle cx="50" cy="12" r="4" fill="white" />
</svg>
</div>
{/* Wordmark */}
<div
style={{
fontFamily: theme.fonts.display,
fontSize: theme.fontSizes.hero,
fontWeight: 700,
color: theme.colors.text,
letterSpacing: -2,
}}
>
SportsTime
</div>
</div>
{/* Tagline */}
{tagline && (
<div
style={{
position: "absolute",
bottom: showAppStoreBadge ? 280 : 200,
opacity: taglineOpacity,
transform: `translateY(${taglineY}px)`,
fontFamily: theme.fonts.text,
fontSize: theme.fontSizes.subtitle,
fontWeight: 500,
color: theme.colors.textSecondary,
letterSpacing: 0.5,
}}
>
{tagline}
</div>
)}
{/* App Store Badge */}
{showAppStoreBadge && (
<div
style={{
position: "absolute",
bottom: 120,
opacity: badgeOpacity,
transform: `scale(${badgeProgress})`,
}}
>
<div
style={{
padding: "16px 32px",
background: theme.colors.text,
borderRadius: 12,
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<svg width="32" height="32" viewBox="0 0 32 32" fill="black">
<path d="M16 2C8.268 2 2 8.268 2 16s6.268 14 14 14 14-6.268 14-14S23.732 2 16 2zm5.788 20.683c-.314.467-.782.7-1.404.7-.311 0-.622-.078-.933-.233l-3.451-1.867-3.451 1.867c-.311.155-.622.233-.933.233-.622 0-1.09-.233-1.404-.7-.314-.467-.389-1.012-.225-1.635l.933-3.917-3.062-2.567c-.467-.389-.7-.856-.7-1.4 0-.544.233-1.012.7-1.4l3.062-2.567-.933-3.917c-.164-.623-.089-1.168.225-1.635.314-.467.782-.7 1.404-.7.311 0 .622.078.933.233L16 5.142l3.451-1.867c.311-.155.622-.233.933-.233.622 0 1.09.233 1.404.7.314.467.389 1.012.225 1.635l-.933 3.917 3.062 2.567c.467.388.7.856.7 1.4 0 .544-.233 1.011-.7 1.4l-3.062 2.567.933 3.917c.164.623.089 1.168-.225 1.635z" />
</svg>
<span
style={{
fontFamily: theme.fonts.text,
fontSize: 20,
fontWeight: 600,
color: "black",
}}
>
Download on the App Store
</span>
</div>
</div>
)}
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,173 @@
import React from "react";
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type TapIndicatorProps = {
x: number;
y: number;
delay?: number;
showRipple?: boolean;
};
export const TapIndicator: React.FC<TapIndicatorProps> = ({
x,
y,
delay = 0,
showRipple = true,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const localFrame = frame - delayFrames;
if (localFrame < 0) {
return null;
}
// Finger appears
const fingerProgress = spring({
frame: localFrame,
fps,
config: theme.animation.smooth,
});
// Finger presses down
const pressProgress = spring({
frame: localFrame - fps * 0.2,
fps,
config: { damping: 30, stiffness: 300 },
});
const fingerScale = interpolate(fingerProgress, [0, 1], [0.5, 1]);
const fingerOpacity = interpolate(fingerProgress, [0, 1], [0, 1]);
const pressScale = interpolate(pressProgress, [0, 1], [1, 0.9]);
// Ripple effect
const rippleProgress = interpolate(
localFrame,
[fps * 0.25, fps * 0.7],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const rippleScale = interpolate(rippleProgress, [0, 1], [0.5, 2]);
const rippleOpacity = interpolate(rippleProgress, [0, 0.3, 1], [0, 0.5, 0]);
return (
<div
style={{
position: "absolute",
left: x,
top: y,
transform: "translate(-50%, -50%)",
pointerEvents: "none",
}}
>
{/* Ripple */}
{showRipple && rippleProgress > 0 && (
<div
style={{
position: "absolute",
width: 80,
height: 80,
borderRadius: "50%",
background: theme.colors.accent,
opacity: rippleOpacity,
transform: `translate(-50%, -50%) scale(${rippleScale})`,
left: "50%",
top: "50%",
}}
/>
)}
{/* Finger circle */}
<div
style={{
width: 60,
height: 60,
borderRadius: "50%",
background: `rgba(255, 255, 255, 0.9)`,
boxShadow: `0 4px 20px rgba(0, 0, 0, 0.3)`,
opacity: fingerOpacity,
transform: `scale(${fingerScale * pressScale})`,
}}
/>
</div>
);
};
// Swipe indicator for tutorial-style animations
type SwipeIndicatorProps = {
startX: number;
startY: number;
endX: number;
endY: number;
delay?: number;
duration?: number;
};
export const SwipeIndicator: React.FC<SwipeIndicatorProps> = ({
startX,
startY,
endX,
endY,
delay = 0,
duration = 0.8,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const durationFrames = duration * fps;
const localFrame = frame - delayFrames;
if (localFrame < 0 || localFrame > durationFrames + fps * 0.3) {
return null;
}
const progress = interpolate(
localFrame,
[0, durationFrames],
[0, 1],
{ extrapolateRight: "clamp" }
);
const x = interpolate(progress, [0, 1], [startX, endX]);
const y = interpolate(progress, [0, 1], [startY, endY]);
const opacity = interpolate(
localFrame,
[0, fps * 0.1, durationFrames, durationFrames + fps * 0.2],
[0, 1, 1, 0],
{ extrapolateRight: "clamp" }
);
return (
<div
style={{
position: "absolute",
left: x,
top: y,
transform: "translate(-50%, -50%)",
pointerEvents: "none",
opacity,
}}
>
<div
style={{
width: 50,
height: 50,
borderRadius: "50%",
background: `rgba(255, 255, 255, 0.9)`,
boxShadow: `0 4px 20px rgba(0, 0, 0, 0.3)`,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,188 @@
import React from "react";
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type TextRevealProps = {
text: string;
fontSize?: number;
color?: string;
fontWeight?: number;
textAlign?: "left" | "center" | "right";
delay?: number;
style?: React.CSSProperties;
};
export const TextReveal: React.FC<TextRevealProps> = ({
text,
fontSize = theme.fontSizes.title,
color = theme.colors.text,
fontWeight = 700,
textAlign = "center",
delay = 0,
style = {},
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
// Spring for smooth entrance
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.smooth,
});
const opacity = interpolate(
frame - delayFrames,
[0, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const translateY = interpolate(progress, [0, 1], [30, 0]);
if (frame < delayFrames) {
return null;
}
return (
<div
style={{
fontFamily: theme.fonts.display,
fontSize,
fontWeight,
color,
textAlign,
opacity,
transform: `translateY(${translateY}px)`,
letterSpacing: -1,
lineHeight: 1.2,
...style,
}}
>
{text}
</div>
);
};
// Multi-line version with staggered reveal
type TextRevealMultilineProps = {
lines: string[];
fontSize?: number;
color?: string;
fontWeight?: number;
textAlign?: "left" | "center" | "right";
staggerDelay?: number;
startDelay?: number;
lineHeight?: number;
};
export const TextRevealMultiline: React.FC<TextRevealMultilineProps> = ({
lines,
fontSize = theme.fontSizes.title,
color = theme.colors.text,
fontWeight = 700,
textAlign = "center",
staggerDelay = 0.1,
startDelay = 0,
lineHeight = 1.3,
}) => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: textAlign === "center" ? "center" : textAlign === "right" ? "flex-end" : "flex-start",
gap: 8,
}}
>
{lines.map((line, index) => (
<TextReveal
key={index}
text={line}
fontSize={fontSize}
color={color}
fontWeight={fontWeight}
textAlign={textAlign}
delay={startDelay + index * staggerDelay}
style={{ lineHeight }}
/>
))}
</div>
);
};
// Highlight style text (with background)
type HighlightTextProps = {
text: string;
fontSize?: number;
delay?: number;
};
export const HighlightText: React.FC<HighlightTextProps> = ({
text,
fontSize = theme.fontSizes.subtitle,
delay = 0,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.snappy,
});
const scaleX = interpolate(progress, [0, 1], [0, 1]);
const textOpacity = interpolate(
frame - delayFrames,
[fps * 0.15, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
if (frame < delayFrames) {
return null;
}
return (
<div style={{ position: "relative", display: "inline-block" }}>
{/* Background highlight */}
<div
style={{
position: "absolute",
top: 0,
left: -12,
right: -12,
bottom: 0,
background: theme.colors.accent,
borderRadius: 8,
transform: `scaleX(${scaleX})`,
transformOrigin: "left",
}}
/>
{/* Text */}
<span
style={{
position: "relative",
fontFamily: theme.fonts.display,
fontSize,
fontWeight: 700,
color: theme.colors.text,
opacity: textOpacity,
padding: "4px 0",
}}
>
{text}
</span>
</div>
);
};

View File

@@ -0,0 +1,6 @@
export { theme } from "./theme";
export { LogoEndcard } from "./LogoEndcard";
export { TextReveal, TextRevealMultiline, HighlightText } from "./TextReveal";
export { TapIndicator, SwipeIndicator } from "./TapIndicator";
export { AppScreenshot, MockScreen } from "./AppScreenshot";
export { GradientBackground, GridBackground, GlowBackground } from "./Background";

View File

@@ -0,0 +1,47 @@
// SportsTime Marketing Video Theme
export const theme = {
colors: {
background: "#0A0A0A",
backgroundGradientStart: "#0A0A0A",
backgroundGradientEnd: "#1A1A2E",
accent: "#FF6B35",
accentDark: "#E55A25",
secondary: "#4ECDC4",
secondaryDark: "#3DBDB5",
text: "#FFFFFF",
textSecondary: "#B0B0B0",
textMuted: "#6B6B6B",
success: "#4CAF50",
gold: "#FFD700",
mapLine: "#FF6B35",
mapMarker: "#4ECDC4",
},
fonts: {
display: "SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif",
text: "SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif",
},
fontSizes: {
hero: 72,
headline: 56,
title: 48,
subtitle: 36,
body: 28,
caption: 20,
},
spacing: {
xs: 8,
sm: 16,
md: 24,
lg: 40,
xl: 64,
xxl: 96,
},
animation: {
smooth: { damping: 200 },
snappy: { damping: 20, stiffness: 200 },
bouncy: { damping: 8 },
heavy: { damping: 15, stiffness: 80, mass: 2 },
},
} as const;
export type Theme = typeof theme;