Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
73
marketing-video/README.md
Normal file
73
marketing-video/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# ScanReceipts — Marketing Video
|
||||
|
||||
A cinematic 35-second hype/marketing video built with [Remotion](https://remotion.dev), showcasing the ScanReceipts AI receipt-to-Excel app.
|
||||
|
||||
## 🎬 Video Structure
|
||||
|
||||
| Scene | Frames | Duration | Description |
|
||||
|-------|--------|----------|-------------|
|
||||
| 01 — Intro | 0–215 | 7s | Logo reveal + animated headlines + receipts flying in + live AI scan |
|
||||
| 02 — Problem | 210–405 | 6.5s | Dark panel with stat counters (hours/€ wasted) + pain-point cards |
|
||||
| 03 — Magic | 390–605 | 7s | 3-panel flow: Receipt → AI Extraction fields → Excel export |
|
||||
| 04 — Features | 600–815 | 7s | 6-card feature grid with staggered spring animations |
|
||||
| 05 — Social Proof | 810–965 | 5s | Dark scene with KPI stats + testimonial cards |
|
||||
| 06 — CTA | 960–1050 | 3s | Punchy CTA with typewriter URL + pulsing glow button |
|
||||
|
||||
## 🚀 Commands
|
||||
|
||||
```bash
|
||||
# Open Remotion Studio (live preview at http://localhost:3030)
|
||||
npm start
|
||||
|
||||
# Render the video (standard quality)
|
||||
npm run render
|
||||
|
||||
# Render high-quality (CRF 14, quality 100)
|
||||
npm run render:hq
|
||||
|
||||
# Export a single frame as PNG
|
||||
npm run still
|
||||
```
|
||||
|
||||
## 📐 Specs
|
||||
|
||||
- **Resolution**: 1920 × 1080 (16:9 Full HD)
|
||||
- **FPS**: 30
|
||||
- **Duration**: 35 seconds (1050 frames)
|
||||
- **Fonts**: Hanken Grotesk · Inter · JetBrains Mono (via @remotion/google-fonts)
|
||||
- **Design System**: Zenith Silver (matches the ScanReceipts brand)
|
||||
|
||||
## 🎨 Design Tokens
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| Background | `#F6F9FF` | Page background |
|
||||
| Surface | `#FFFFFF` | Cards / modals |
|
||||
| Border | `#E2E8F0` | Dividers |
|
||||
| Ink | `#161C22` | Body text |
|
||||
| Accent | `#000000` | Headlines, buttons |
|
||||
| Success | `#059669` | Status badges, scan line |
|
||||
| Ink-2 | `#475569` | Subtext |
|
||||
|
||||
## 📁 Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Remotion entry point
|
||||
├── Root.tsx # Composition registry
|
||||
├── MarketingVideo.tsx # Main video with all Sequences
|
||||
├── components/
|
||||
│ ├── AnimatedHeadline.tsx # Word-by-word stagger animation
|
||||
│ ├── Background.tsx # Scrolling grid background
|
||||
│ ├── ExcelMock.tsx # Spreadsheet UI mock
|
||||
│ ├── FontLoader.tsx # Google Fonts preloader
|
||||
│ ├── ReceiptMock.tsx # Animated receipt with scanline
|
||||
│ └── Reveal.tsx # Spring-based entrance animation
|
||||
└── scenes/
|
||||
├── Scene01Intro.tsx # Hero scene
|
||||
├── Scene02Problem.tsx # Pain points + counters
|
||||
├── Scene03Magic.tsx # AI demo flow
|
||||
├── Scene04Features.tsx # Feature grid
|
||||
├── Scene05Social.tsx # Social proof
|
||||
└── Scene06CTA.tsx # Call to action
|
||||
```
|
||||
3759
marketing-video/package-lock.json
generated
Normal file
3759
marketing-video/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
marketing-video/package.json
Normal file
24
marketing-video/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "marketing-video",
|
||||
"version": "1.0.0",
|
||||
"description": "ScanReceipts hype marketing video built with Remotion",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "npx remotion studio",
|
||||
"render": "npx remotion render ScanReceiptsMarketing out/marketing.mp4",
|
||||
"render:hq": "npx remotion render ScanReceiptsMarketing out/marketing-hq.mp4 --quality 100 --crf 14",
|
||||
"still": "npx remotion still ScanReceiptsMarketing out/still.png",
|
||||
"build": "npx tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@remotion/cli": "^4.0.0",
|
||||
"@remotion/google-fonts": "^4.0.512",
|
||||
"@remotion/renderer": "^4.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"remotion": "^4.0.0"
|
||||
}
|
||||
}
|
||||
9
marketing-video/remotion.config.ts
Normal file
9
marketing-video/remotion.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Config } from '@remotion/cli/config';
|
||||
|
||||
Config.setEntryPoint('./src/index.ts');
|
||||
|
||||
// High quality output defaults
|
||||
Config.setVideoImageFormat('jpeg');
|
||||
Config.setJpegQuality(95);
|
||||
Config.setScale(1);
|
||||
Config.setChromiumOpenGlRenderer('angle');
|
||||
55
marketing-video/src/MarketingVideo.tsx
Normal file
55
marketing-video/src/MarketingVideo.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, Sequence } from 'remotion';
|
||||
import { Scene01Intro } from './scenes/Scene01Intro';
|
||||
import { Scene02Problem } from './scenes/Scene02Problem';
|
||||
import { Scene03Magic } from './scenes/Scene03Magic';
|
||||
import { Scene04Features } from './scenes/Scene04Features';
|
||||
import { Scene05Social } from './scenes/Scene05Social';
|
||||
import { Scene06CTA } from './scenes/Scene06CTA';
|
||||
import { Background } from './components/Background';
|
||||
import { FontLoader } from './components/FontLoader';
|
||||
|
||||
// 35 seconds @ 30fps = 1050 frames
|
||||
// Scene timing:
|
||||
// 0 – 215 (7.2s): Intro / Hero
|
||||
// 210 – 405 (6.5s): The Problem
|
||||
// 390 – 605 (7.2s): The Magic (AI scanning)
|
||||
// 600 – 815 (7.2s): Features showcase
|
||||
// 810 – 965 (5.2s): Social proof
|
||||
// 960 – 1050 (3s) : CTA
|
||||
|
||||
export const MarketingVideo: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill style={{ background: '#F6F9FF', fontFamily: "'Hanken Grotesk', sans-serif" }}>
|
||||
{/* Preload Google Fonts for pixel-perfect rendering */}
|
||||
<FontLoader />
|
||||
|
||||
{/* Global background grid always present */}
|
||||
<Background />
|
||||
|
||||
<Sequence from={0} durationInFrames={215}>
|
||||
<Scene01Intro />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={210} durationInFrames={195}>
|
||||
<Scene02Problem />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={390} durationInFrames={215}>
|
||||
<Scene03Magic />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={600} durationInFrames={215}>
|
||||
<Scene04Features />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={810} durationInFrames={155}>
|
||||
<Scene05Social />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={960} durationInFrames={90}>
|
||||
<Scene06CTA />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
19
marketing-video/src/Root.tsx
Normal file
19
marketing-video/src/Root.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Composition } from 'remotion';
|
||||
import { MarketingVideo } from './MarketingVideo';
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Composition
|
||||
id="ScanReceiptsMarketing"
|
||||
component={MarketingVideo}
|
||||
durationInFrames={1050}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
47
marketing-video/src/components/AnimatedHeadline.tsx
Normal file
47
marketing-video/src/components/AnimatedHeadline.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
delay?: number;
|
||||
style?: React.CSSProperties;
|
||||
staggerMs?: number;
|
||||
}
|
||||
|
||||
// Animates each word in individually
|
||||
export const AnimatedHeadline: React.FC<Props> = ({ text, delay = 0, style = {}, staggerMs = 3 }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const words = text.split(' ');
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25em 0.2em', ...style }}>
|
||||
{words.map((word, i) => {
|
||||
const wordDelay = delay + i * staggerMs;
|
||||
const progress = spring({
|
||||
frame: frame - wordDelay,
|
||||
fps,
|
||||
config: { damping: 16, stiffness: 150, mass: 0.6 },
|
||||
});
|
||||
const opacity = interpolate(frame - wordDelay, [0, 10], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
const translateY = interpolate(progress, [0, 1], [50, 0]);
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
opacity,
|
||||
transform: `translateY(${translateY}px)`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
marketing-video/src/components/Background.tsx
Normal file
34
marketing-video/src/components/Background.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
|
||||
|
||||
export const Background: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const drift = interpolate(frame, [0, 1050], [0, -40], { extrapolateRight: 'clamp' });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ pointerEvents: 'none', overflow: 'hidden' }}>
|
||||
{/* Subtle grid */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '-10%',
|
||||
backgroundImage:
|
||||
'linear-gradient(#E2E8F0 1px, transparent 1px), linear-gradient(90deg, #E2E8F0 1px, transparent 1px)',
|
||||
backgroundSize: '80px 80px',
|
||||
opacity: 0.28,
|
||||
transform: `translateY(${drift}px)`,
|
||||
}}
|
||||
/>
|
||||
{/* Radial vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'radial-gradient(ellipse 80% 80% at 50% 50%, transparent 40%, rgba(246,249,255,0.7) 100%)',
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
156
marketing-video/src/components/ExcelMock.tsx
Normal file
156
marketing-video/src/components/ExcelMock.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
rows: { vendor: string; date: string; category: string; amount: string; status: 'confirmed' | 'pending' | 'scanned' }[];
|
||||
highlightRow?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
const STATUS_COLORS = {
|
||||
confirmed: { bg: '#F0FDF4', text: '#15803D', border: '#BBF7D0' },
|
||||
pending: { bg: '#FFFBEB', text: '#B45309', border: '#FDE68A' },
|
||||
scanned: { bg: '#EFF6FF', text: '#1D4ED8', border: '#BFDBFE' },
|
||||
};
|
||||
|
||||
export const ExcelMock: React.FC<Props> = ({ rows, highlightRow, scale = 1 }) => {
|
||||
const cols = ['#', 'VENDOR', 'DATE', 'CATEGORY', 'AMOUNT', 'STATUS'];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid #E2E8F0',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.2)',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
}}
|
||||
>
|
||||
{/* Toolbar */}
|
||||
<div
|
||||
style={{
|
||||
background: '#F8FAFC',
|
||||
borderBottom: '1px solid #E2E8F0',
|
||||
padding: `${7 * scale}px ${12 * scale}px`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8 * scale,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9 * scale,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase' as const,
|
||||
color: '#64748B',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid #E2E8F0',
|
||||
borderBottom: 'none',
|
||||
padding: `${3 * scale}px ${8 * scale}px`,
|
||||
color: '#000000',
|
||||
fontSize: 9 * scale,
|
||||
}}
|
||||
>
|
||||
Sheet1
|
||||
</div>
|
||||
<div style={{ padding: `${3 * scale}px ${8 * scale}px` }}>DATEV</div>
|
||||
<div style={{ marginLeft: 'auto', color: '#059669', display: 'flex', alignItems: 'center', gap: 4 * scale }}>
|
||||
<div style={{ width: 6 * scale, height: 6 * scale, borderRadius: '50%', background: '#059669' }} />
|
||||
AUTO-SAVED
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{cols.map((col) => (
|
||||
<th
|
||||
key={col}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: `${6 * scale}px ${10 * scale}px`,
|
||||
borderBottom: '1px solid #E2E8F0',
|
||||
borderRight: '1px solid #E2E8F0',
|
||||
background: '#F8FAFC',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 8 * scale,
|
||||
letterSpacing: '0.1em',
|
||||
color: '#64748B',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => {
|
||||
const isHighlighted = highlightRow === i;
|
||||
const s = STATUS_COLORS[row.status];
|
||||
return (
|
||||
<tr
|
||||
key={i}
|
||||
style={{
|
||||
background: isHighlighted ? '#F0F9FF' : i % 2 === 0 ? '#FFFFFF' : '#FAFBFC',
|
||||
borderLeft: isHighlighted ? `3px solid #3B82F6` : '3px solid transparent',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<td style={cellStyle(scale)}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 9 * scale, color: '#94A3B8' }}>
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...cellStyle(scale), fontWeight: 600, color: '#161C22', fontSize: 11 * scale }}>
|
||||
{row.vendor}
|
||||
</td>
|
||||
<td style={{ ...cellStyle(scale), fontFamily: 'monospace', fontSize: 9 * scale, color: '#475569' }}>
|
||||
{row.date}
|
||||
</td>
|
||||
<td style={{ ...cellStyle(scale), fontSize: 10 * scale, color: '#475569' }}>{row.category}</td>
|
||||
<td
|
||||
style={{
|
||||
...cellStyle(scale),
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11 * scale,
|
||||
fontWeight: 700,
|
||||
color: '#161C22',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{row.amount}
|
||||
</td>
|
||||
<td style={cellStyle(scale)}>
|
||||
<span
|
||||
style={{
|
||||
background: s.bg,
|
||||
color: s.text,
|
||||
border: `1px solid ${s.border}`,
|
||||
padding: `${2 * scale}px ${6 * scale}px`,
|
||||
fontSize: 8 * scale,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase' as const,
|
||||
}}
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const cellStyle = (scale: number): React.CSSProperties => ({
|
||||
padding: `${7 * scale}px ${10 * scale}px`,
|
||||
borderBottom: '1px solid #EDF0F4',
|
||||
borderRight: '1px solid #EDF0F4',
|
||||
fontSize: 11 * scale,
|
||||
color: '#475569',
|
||||
});
|
||||
11
marketing-video/src/components/FontLoader.tsx
Normal file
11
marketing-video/src/components/FontLoader.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill } from 'remotion';
|
||||
import { loadFont as loadHanken } from '@remotion/google-fonts/HankenGrotesk';
|
||||
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
|
||||
import { loadFont as loadJetBrains } from '@remotion/google-fonts/JetBrainsMono';
|
||||
|
||||
loadHanken();
|
||||
loadInter();
|
||||
loadJetBrains();
|
||||
|
||||
export const FontLoader: React.FC = () => null;
|
||||
129
marketing-video/src/components/ReceiptMock.tsx
Normal file
129
marketing-video/src/components/ReceiptMock.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
|
||||
|
||||
interface ReceiptProps {
|
||||
vendor: string;
|
||||
date: string;
|
||||
items: { name: string; amount: string }[];
|
||||
total: string;
|
||||
scanning?: boolean;
|
||||
scanProgress?: number; // 0–1
|
||||
style?: React.CSSProperties;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export const ReceiptMock: React.FC<ReceiptProps> = ({
|
||||
vendor,
|
||||
date,
|
||||
items,
|
||||
total,
|
||||
scanning = false,
|
||||
scanProgress = 0,
|
||||
style = {},
|
||||
scale = 1,
|
||||
}) => {
|
||||
const scanY = `${scanProgress * 90 + 5}%`;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid #E2E8F0',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: 13 * scale,
|
||||
width: 220 * scale,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.25)',
|
||||
transform: scanning ? `translateY(${-6 * scale}px)` : 'translateY(0)',
|
||||
transition: 'transform 0.4s ease',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{/* Scan line */}
|
||||
{scanning && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: scanY,
|
||||
height: 2,
|
||||
background: '#059669',
|
||||
boxShadow: '0 0 14px 3px rgba(5,150,105,0.55)',
|
||||
zIndex: 10,
|
||||
transition: 'top 0.05s linear',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #E2E8F0',
|
||||
padding: `${12 * scale}px ${14 * scale}px`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 700,
|
||||
fontSize: 16 * scale,
|
||||
letterSpacing: '-0.01em',
|
||||
color: '#161C22',
|
||||
}}
|
||||
>
|
||||
{vendor}
|
||||
</div>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 9 * scale, color: '#64748B' }}>{date}</div>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div style={{ padding: `${10 * scale}px ${14 * scale}px` }}>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: `${4 * scale}px 0`,
|
||||
borderBottom: '1px dotted #EEF1F5',
|
||||
color: '#161C22',
|
||||
fontSize: 11 * scale,
|
||||
}}
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<span style={{ fontFamily: 'monospace', fontWeight: 600 }}>{item.amount}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: `${10 * scale}px ${14 * scale}px`,
|
||||
borderTop: '2px solid #161C22',
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
fontSize: 13 * scale,
|
||||
color: '#161C22',
|
||||
}}
|
||||
>
|
||||
<span>TOTAL</span>
|
||||
<span>{total}</span>
|
||||
</div>
|
||||
|
||||
{/* Receipt tape perforations */}
|
||||
<div style={{ display: 'flex', gap: 4 * scale, padding: `0 ${14 * scale}px ${8 * scale}px`, opacity: 0.3 }}>
|
||||
{Array.from({ length: 14 }).map((_, i) => (
|
||||
<div key={i} style={{ width: 4 * scale, height: 4 * scale, borderRadius: '50%', background: '#94A3B8' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
marketing-video/src/components/Reveal.tsx
Normal file
48
marketing-video/src/components/Reveal.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
direction?: 'up' | 'down' | 'left' | 'right' | 'scale' | 'fade';
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export const Reveal: React.FC<Props> = ({
|
||||
children,
|
||||
delay = 0,
|
||||
direction = 'up',
|
||||
duration = 20,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const progress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: {
|
||||
damping: 18,
|
||||
stiffness: 120,
|
||||
mass: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
const opacity = interpolate(frame - delay, [0, 12], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
let transform = '';
|
||||
if (direction === 'up') transform = `translateY(${interpolate(progress, [0, 1], [40, 0])}px)`;
|
||||
if (direction === 'down') transform = `translateY(${interpolate(progress, [0, 1], [-40, 0])}px)`;
|
||||
if (direction === 'left') transform = `translateX(${interpolate(progress, [0, 1], [60, 0])}px)`;
|
||||
if (direction === 'right') transform = `translateX(${interpolate(progress, [0, 1], [-60, 0])}px)`;
|
||||
if (direction === 'scale') transform = `scale(${interpolate(progress, [0, 1], [0.85, 1])})`;
|
||||
if (direction === 'fade') transform = '';
|
||||
|
||||
return (
|
||||
<div style={{ opacity, transform }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
4
marketing-video/src/index.ts
Normal file
4
marketing-video/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from 'remotion';
|
||||
import { RemotionRoot } from './Root';
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
360
marketing-video/src/scenes/Scene01Intro.tsx
Normal file
360
marketing-video/src/scenes/Scene01Intro.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AbsoluteFill,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
} from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
import { ReceiptMock } from '../components/ReceiptMock';
|
||||
|
||||
// Scene 01: Cinematic Intro — 0–210 frames (7s)
|
||||
// Big logo reveal + headline + animated receipts flying in
|
||||
|
||||
export const Scene01Intro: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Scene fade-in
|
||||
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
|
||||
|
||||
// Logo spring
|
||||
const logoSpring = spring({ frame: frame - 5, fps, config: { damping: 14, stiffness: 100, mass: 0.8 } });
|
||||
const logoScale = interpolate(logoSpring, [0, 1], [0.5, 1]);
|
||||
const logoOpacity = interpolate(frame - 5, [0, 18], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Receipt 1 — flies in from left
|
||||
const r1Spring = spring({ frame: frame - 30, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
|
||||
const r1x = interpolate(r1Spring, [0, 1], [-350, 0]);
|
||||
const r1rot = interpolate(r1Spring, [0, 1], [-15, -8]);
|
||||
const r1opacity = interpolate(frame - 30, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Receipt 2 — flies in from right
|
||||
const r2Spring = spring({ frame: frame - 50, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
|
||||
const r2x = interpolate(r2Spring, [0, 1], [350, 0]);
|
||||
const r2rot = interpolate(r2Spring, [0, 1], [12, 6]);
|
||||
const r2opacity = interpolate(frame - 50, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Receipt 3 — flies in from bottom
|
||||
const r3Spring = spring({ frame: frame - 70, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
|
||||
const r3y = interpolate(r3Spring, [0, 1], [300, 0]);
|
||||
const r3rot = interpolate(r3Spring, [0, 1], [20, 3]);
|
||||
const r3opacity = interpolate(frame - 70, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Scanline over stacked receipts
|
||||
const scanStart = 90;
|
||||
const scanProgress = interpolate(frame - scanStart, [0, 60], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
const scanning = frame > scanStart;
|
||||
|
||||
// Scene exit fade
|
||||
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
|
||||
{/* Left half: Text content */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '0 80px',
|
||||
}}
|
||||
>
|
||||
{/* Logo / Brand */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
marginBottom: 40,
|
||||
opacity: logoOpacity,
|
||||
transform: `scale(${logoScale})`,
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 54,
|
||||
height: 54,
|
||||
border: '2.5px solid #000000',
|
||||
background: '#FFFFFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 18,
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
SR
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 22,
|
||||
letterSpacing: '-0.03em',
|
||||
color: '#000000',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
ScanReceipts
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
AI-Powered Bookkeeping
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badge */}
|
||||
<Reveal delay={20}>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
border: '1px solid #E2E8F0',
|
||||
background: '#EEF4FC',
|
||||
padding: '7px 12px',
|
||||
marginBottom: 22,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#161C22',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 7, height: 7, background: '#059669' }} />
|
||||
New · AI Receipt Scanner
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Main headline */}
|
||||
<AnimatedHeadline
|
||||
text="One Photo."
|
||||
delay={28}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 82,
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: 1.0,
|
||||
color: '#000000',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
/>
|
||||
<AnimatedHeadline
|
||||
text="Perfect Excel."
|
||||
delay={40}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 82,
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: 1.0,
|
||||
color: '#475569',
|
||||
marginBottom: 32,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Subline */}
|
||||
<Reveal delay={65}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: '#475569',
|
||||
lineHeight: 1.6,
|
||||
maxWidth: 420,
|
||||
marginBottom: 40,
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
}}
|
||||
>
|
||||
Snap a receipt. AI reads vendor, date, items, and tax — then exports a flawless dual-sheet Excel in seconds.
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Proof chips */}
|
||||
<Reveal delay={80}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 16,
|
||||
flexWrap: 'wrap',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#475569',
|
||||
borderTop: '1px solid #E2E8F0',
|
||||
paddingTop: 22,
|
||||
}}
|
||||
>
|
||||
{['✓ DATEV-Compatible', '✓ 99% AI Accuracy', '✓ Instant Export', '✓ Zero Manual Work'].map((t, i) => (
|
||||
<span key={i} style={{ color: i === 0 ? '#059669' : '#475569', fontWeight: i === 0 ? 700 : 400 }}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Right half: Animated receipts stack */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', width: 340, height: 460 }}>
|
||||
{/* Receipt 3 — back */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 80,
|
||||
opacity: r3opacity,
|
||||
transform: `translateY(${r3y}px) rotate(${r3rot}deg)`,
|
||||
}}
|
||||
>
|
||||
<ReceiptMock
|
||||
vendor="REWE"
|
||||
date="17.08.2026"
|
||||
items={[
|
||||
{ name: 'Milch 1L', amount: '€1.29' },
|
||||
{ name: 'Brot', amount: '€2.49' },
|
||||
{ name: 'MwSt. 7%', amount: '€0.27' },
|
||||
]}
|
||||
total="€3.78"
|
||||
scale={0.88}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Receipt 1 — left */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: -20,
|
||||
opacity: r1opacity,
|
||||
transform: `translateX(${r1x}px) rotate(${r1rot}deg)`,
|
||||
}}
|
||||
>
|
||||
<ReceiptMock
|
||||
vendor="Trattoria Roma"
|
||||
date="15.08.2026"
|
||||
items={[
|
||||
{ name: 'Pasta Carbonara', amount: '€14.90' },
|
||||
{ name: 'Vino Rosso', amount: '€8.50' },
|
||||
{ name: 'MwSt. 19%', amount: '€4.44' },
|
||||
]}
|
||||
total="€27.84"
|
||||
scale={0.85}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Receipt 2 — right, SCANNING */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
right: -30,
|
||||
opacity: r2opacity,
|
||||
transform: `translateX(${r2x}px) rotate(${r2rot}deg)`,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<ReceiptMock
|
||||
vendor="Aral Tankstelle"
|
||||
date="16.08.2026"
|
||||
items={[
|
||||
{ name: 'Super E10 — 48L', amount: '€82.08' },
|
||||
{ name: 'MwSt. 19%', amount: '€13.10' },
|
||||
]}
|
||||
total="€95.18"
|
||||
scanning={scanning}
|
||||
scanProgress={scanProgress}
|
||||
scale={0.92}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI Processing badge */}
|
||||
{frame > 110 && (
|
||||
<Reveal delay={0}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '#000000',
|
||||
color: '#FFFFFF',
|
||||
padding: '10px 18px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: '50%',
|
||||
background: '#059669',
|
||||
animation: 'pulse 1.5s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
AI Extracting Data...
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vertical divider line */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '10%',
|
||||
width: 1,
|
||||
height: '80%',
|
||||
background: '#E2E8F0',
|
||||
opacity: interpolate(frame, [40, 70], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
243
marketing-video/src/scenes/Scene02Problem.tsx
Normal file
243
marketing-video/src/scenes/Scene02Problem.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
|
||||
// Scene 02: The Problem — 210–390 frames (6s)
|
||||
// Show the pain: manual receipt tracking is a nightmare
|
||||
|
||||
const PAIN_POINTS = [
|
||||
{ icon: '📁', text: 'Receipts piling up in shoeboxes', sub: 'Average accountant wastes 4h/week sorting' },
|
||||
{ icon: '⌨️', text: 'Manual data entry — typo-prone', sub: 'One error = rejected expense claim' },
|
||||
{ icon: '🗓️', text: 'Tax season panic & missing docs', sub: '43% of SMBs pay late-filing penalties' },
|
||||
{ icon: '💸', text: 'Accountant bills through the roof', sub: 'Up to €180/hour for data entry' },
|
||||
];
|
||||
|
||||
export const Scene02Problem: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
|
||||
const exitFade = interpolate(frame, [175, 195], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Headline counter animation
|
||||
const counterProgress = interpolate(frame, [40, 120], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
easing: (t) => 1 - Math.pow(1 - t, 3),
|
||||
});
|
||||
const hoursWasted = Math.round(counterProgress * 4);
|
||||
const costWasted = Math.round(counterProgress * 2880);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
|
||||
{/* Dark panel left */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: interpolate(frame, [0, 25], [0, 44], { extrapolateRight: 'clamp' }) + '%',
|
||||
height: '100%',
|
||||
background: '#000000',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '0 64px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Reveal delay={18}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#EF4444',
|
||||
marginBottom: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 26, height: 1, background: '#EF4444' }} />
|
||||
The Problem
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<AnimatedHeadline
|
||||
text="Manual receipts are killing your time."
|
||||
delay={25}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 44,
|
||||
letterSpacing: '-0.03em',
|
||||
lineHeight: 1.1,
|
||||
color: '#FFFFFF',
|
||||
marginBottom: 40,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Stats counters */}
|
||||
<Reveal delay={45}>
|
||||
<div style={{ display: 'flex', gap: 32, marginBottom: 40 }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 56,
|
||||
letterSpacing: '-0.04em',
|
||||
color: '#EF4444',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{hoursWasted}h
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94A3B8',
|
||||
marginTop: 6,
|
||||
}}
|
||||
>
|
||||
Wasted per week
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 1, background: '#1E293B' }} />
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 56,
|
||||
letterSpacing: '-0.04em',
|
||||
color: '#EF4444',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
€{costWasted.toLocaleString()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94A3B8',
|
||||
marginTop: 6,
|
||||
}}
|
||||
>
|
||||
Avg. annual cost
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={60}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: '#475569',
|
||||
letterSpacing: '0.05em',
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
Sound familiar? You're not alone.
|
||||
<br />
|
||||
<span style={{ color: '#64748B' }}>Over 2 million SMBs still do this manually.</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Right side: pain point cards */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: '56%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '0 72px 0 48px',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Reveal delay={8}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94A3B8',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 26, height: 1, background: '#E2E8F0' }} />
|
||||
Sound familiar?
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{PAIN_POINTS.map((point, i) => {
|
||||
const cardSpring = spring({
|
||||
frame: frame - (30 + i * 18),
|
||||
fps,
|
||||
config: { damping: 16, stiffness: 100, mass: 0.8 },
|
||||
});
|
||||
const cardOpacity = interpolate(frame - (30 + i * 18), [0, 14], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
const cardX = interpolate(cardSpring, [0, 1], [80, 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
opacity: cardOpacity,
|
||||
transform: `translateX(${cardX}px)`,
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid #E2E8F0',
|
||||
padding: '20px 24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 18,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 28, lineHeight: 1 }}>{point.icon}</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
color: '#161C22',
|
||||
letterSpacing: '-0.01em',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{point.text}
|
||||
</div>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 10, color: '#EF4444', letterSpacing: '0.06em' }}>
|
||||
{point.sub}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
382
marketing-video/src/scenes/Scene03Magic.tsx
Normal file
382
marketing-video/src/scenes/Scene03Magic.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
import { ReceiptMock } from '../components/ReceiptMock';
|
||||
import { ExcelMock } from '../components/ExcelMock';
|
||||
|
||||
// Scene 03: The Magic — 390–600 (7s)
|
||||
// Shows the AI scanning process: receipt in → data extracted → Excel out
|
||||
|
||||
const EXCEL_ROWS = [
|
||||
{ vendor: 'Aral Tankstelle', date: '16.08.26', category: 'Travel', amount: '€95.18', status: 'confirmed' as const },
|
||||
{ vendor: 'Trattoria Roma', date: '15.08.26', category: 'Dining', amount: '€27.84', status: 'confirmed' as const },
|
||||
{ vendor: 'REWE', date: '17.08.26', category: 'Office', amount: '€3.78', status: 'scanned' as const },
|
||||
{ vendor: 'Media Markt', date: '12.08.26', category: 'Equipment', amount: '€349.00', status: 'pending' as const },
|
||||
];
|
||||
|
||||
const EXTRACTED_FIELDS = [
|
||||
{ label: 'VENDOR', value: 'Aral Tankstelle München', confirmed: true },
|
||||
{ label: 'DATE', value: '16.08.2026', confirmed: true },
|
||||
{ label: 'AMOUNT', value: '€95.18', confirmed: true },
|
||||
{ label: 'TAX (19%)', value: '€13.10', confirmed: true },
|
||||
{ label: 'CATEGORY', value: 'Travel / Fuel', confirmed: true },
|
||||
{ label: 'PAYMENT', value: 'Credit Card', confirmed: true },
|
||||
];
|
||||
|
||||
export const Scene03Magic: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
|
||||
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
// Scan progress (receipt scan line)
|
||||
const scanProgress = interpolate(frame, [40, 100], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
// Arrow pulse animation
|
||||
const arrowScale = interpolate(
|
||||
Math.sin((frame / 8) * Math.PI),
|
||||
[-1, 1],
|
||||
[0.92, 1.08]
|
||||
);
|
||||
|
||||
// Excel appears at frame 110 with row highlighting cycling
|
||||
const excelOpacity = interpolate(frame, [110, 130], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
const highlightRow = frame > 130 ? Math.floor(((frame - 130) / 30) % 4) : undefined;
|
||||
|
||||
// Fields appear one by one
|
||||
const fieldsVisible = Math.min(6, Math.floor((frame - 50) / 15));
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
|
||||
{/* Header section */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 80,
|
||||
right: 80,
|
||||
}}
|
||||
>
|
||||
<Reveal delay={5}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#059669',
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 26, height: 1, background: '#059669' }} />
|
||||
The Solution
|
||||
</div>
|
||||
</Reveal>
|
||||
<AnimatedHeadline
|
||||
text="Watch the magic happen."
|
||||
delay={10}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 64,
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: 1.05,
|
||||
color: '#000000',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Three-panel demo layout */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 220,
|
||||
left: 80,
|
||||
right: 80,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 40,
|
||||
}}
|
||||
>
|
||||
{/* Panel 1: Receipt */}
|
||||
<div style={{ flex: '0 0 auto' }}>
|
||||
<Reveal delay={20} direction="left">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#000', color: '#fff', padding: '2px 6px', fontSize: 8 }}>01</span>
|
||||
Snap a receipt
|
||||
</div>
|
||||
<ReceiptMock
|
||||
vendor="Aral Tankstelle"
|
||||
date="16.08.2026"
|
||||
items={[
|
||||
{ name: 'Super E10 — 48L', amount: '€82.08' },
|
||||
{ name: 'MwSt. 19%', amount: '€13.10' },
|
||||
]}
|
||||
total="€95.18"
|
||||
scanning={frame > 30}
|
||||
scanProgress={scanProgress}
|
||||
scale={1.05}
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Arrow 1 */}
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
opacity: interpolate(frame, [35, 55], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
|
||||
transform: `scale(${arrowScale})`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 80, height: 1, background: '#000000' }} />
|
||||
<div
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '10px solid #000',
|
||||
borderTop: '6px solid transparent',
|
||||
borderBottom: '6px solid transparent',
|
||||
marginLeft: 80,
|
||||
marginTop: -6,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 8,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#059669',
|
||||
marginTop: -4,
|
||||
background: '#F0FDF4',
|
||||
border: '1px solid #BBF7D0',
|
||||
padding: '3px 8px',
|
||||
}}
|
||||
>
|
||||
AI Processing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panel 2: Extracted fields */}
|
||||
<div style={{ flex: '0 0 auto' }}>
|
||||
<Reveal delay={45}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#059669', color: '#fff', padding: '2px 6px', fontSize: 8 }}>02</span>
|
||||
AI Extracts Data
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid #E2E8F0',
|
||||
width: 260,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.18)',
|
||||
}}
|
||||
>
|
||||
{/* AI header */}
|
||||
<div
|
||||
style={{
|
||||
background: '#000000',
|
||||
padding: '10px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: '50%',
|
||||
background: '#059669',
|
||||
boxShadow: '0 0 8px rgba(5,150,105,0.7)',
|
||||
}}
|
||||
/>
|
||||
Extracted Fields
|
||||
</div>
|
||||
{EXTRACTED_FIELDS.map((field, i) => {
|
||||
const visible = i < fieldsVisible;
|
||||
const fieldProgress = interpolate(frame - (50 + i * 15), [0, 14], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? 'translateX(0)' : 'translateX(20px)',
|
||||
transition: 'all 0.3s ease',
|
||||
padding: '9px 16px',
|
||||
borderBottom: '1px solid #EDF0F4',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 8,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94A3B8',
|
||||
}}
|
||||
>
|
||||
{field.label}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#161C22',
|
||||
}}
|
||||
>
|
||||
{field.value}
|
||||
</span>
|
||||
{visible && (
|
||||
<span style={{ color: '#059669', fontSize: 11, fontWeight: 700 }}>✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Arrow 2 */}
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
opacity: interpolate(frame, [115, 135], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
|
||||
transform: `scale(${arrowScale})`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 80, height: 1, background: '#000000' }} />
|
||||
<div
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '10px solid #000',
|
||||
borderTop: '6px solid transparent',
|
||||
borderBottom: '6px solid transparent',
|
||||
marginLeft: 80,
|
||||
marginTop: -6,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 8,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#059669',
|
||||
marginTop: -4,
|
||||
background: '#F0FDF4',
|
||||
border: '1px solid #BBF7D0',
|
||||
padding: '3px 8px',
|
||||
}}
|
||||
>
|
||||
Auto-Export
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panel 3: Excel */}
|
||||
<div style={{ flex: 1, opacity: excelOpacity, transform: `translateX(${interpolate(excelOpacity, [0, 1], [40, 0])}px)` }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#1D4ED8', color: '#fff', padding: '2px 6px', fontSize: 8 }}>03</span>
|
||||
Perfect Excel
|
||||
</div>
|
||||
<ExcelMock rows={EXCEL_ROWS} highlightRow={highlightRow} scale={0.88} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom timing badge */}
|
||||
<Reveal delay={150}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 60,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '#000000',
|
||||
color: '#FFFFFF',
|
||||
padding: '12px 28px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#059669', fontSize: 20, lineHeight: 1 }}>⚡</span>
|
||||
Total time: Under 3 seconds
|
||||
<span style={{ color: '#059669', fontSize: 20, lineHeight: 1 }}>⚡</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
216
marketing-video/src/scenes/Scene04Features.tsx
Normal file
216
marketing-video/src/scenes/Scene04Features.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
|
||||
// Scene 04: Features Showcase — 600–810 (7s)
|
||||
// Grid of features, each reveals with stagger
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
number: '01',
|
||||
title: 'AI Accuracy Engine',
|
||||
desc: 'Reads vendor, date, line items, VAT, and totals with 99% accuracy across all receipt types.',
|
||||
tag: 'CORE AI',
|
||||
color: '#000000',
|
||||
bg: '#FFFFFF',
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
title: 'Batch Upload Queue',
|
||||
desc: 'Drop 50 receipts at once. Live progress bars, instant thumbnails, one-click retry on errors.',
|
||||
tag: 'PRODUCTIVITY',
|
||||
color: '#1D4ED8',
|
||||
bg: '#EFF6FF',
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
title: 'Dual-Sheet Excel Export',
|
||||
desc: 'Summary sheet + raw data. DATEV-compatible CSV also included — ready for your accountant.',
|
||||
tag: 'EXPORT',
|
||||
color: '#059669',
|
||||
bg: '#F0FDF4',
|
||||
},
|
||||
{
|
||||
number: '04',
|
||||
title: 'Side-by-Side Inspector',
|
||||
desc: 'Zoom, pan, rotate the document while editing extracted fields with 2-way bounding box sync.',
|
||||
tag: 'REVIEW',
|
||||
color: '#7C3AED',
|
||||
bg: '#F5F3FF',
|
||||
},
|
||||
{
|
||||
number: '05',
|
||||
title: 'Smart Filter & Search',
|
||||
desc: 'Filter by date range, category, amount, or status. Instant chip filters. Powerful full-text search.',
|
||||
tag: 'SEARCH',
|
||||
color: '#B45309',
|
||||
bg: '#FFFBEB',
|
||||
},
|
||||
{
|
||||
number: '06',
|
||||
title: 'Offline-First Storage',
|
||||
desc: 'Your data stays on-device in IndexedDB. No cloud dependency. Privacy-first by design.',
|
||||
tag: 'PRIVACY',
|
||||
color: '#475569',
|
||||
bg: '#F8FAFC',
|
||||
},
|
||||
];
|
||||
|
||||
export const Scene04Features: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
|
||||
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
|
||||
{/* Header */}
|
||||
<div style={{ position: 'absolute', top: 64, left: 80, right: 80 }}>
|
||||
<Reveal delay={5}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#475569',
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 26, height: 1, background: '#000' }} />
|
||||
Everything you need
|
||||
</div>
|
||||
</Reveal>
|
||||
<AnimatedHeadline
|
||||
text="Built for real-world accounting."
|
||||
delay={12}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 60,
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: 1.05,
|
||||
color: '#000000',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Feature grid */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 220,
|
||||
left: 80,
|
||||
right: 80,
|
||||
bottom: 60,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gridTemplateRows: 'repeat(2, 1fr)',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{FEATURES.map((feat, i) => {
|
||||
const row = Math.floor(i / 3);
|
||||
const col = i % 3;
|
||||
const cardDelay = 20 + row * 20 + col * 12;
|
||||
|
||||
const cardSpring = spring({
|
||||
frame: frame - cardDelay,
|
||||
fps,
|
||||
config: { damping: 16, stiffness: 100, mass: 0.7 },
|
||||
});
|
||||
const cardOpacity = interpolate(frame - cardDelay, [0, 12], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
const cardY = interpolate(cardSpring, [0, 1], [30, 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
background: feat.bg,
|
||||
border: '1px solid #E2E8F0',
|
||||
padding: '24px 26px',
|
||||
opacity: cardOpacity,
|
||||
transform: `translateY(${cardY}px)`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Number */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 18,
|
||||
right: 20,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 48,
|
||||
fontWeight: 700,
|
||||
color: feat.color,
|
||||
opacity: 0.08,
|
||||
lineHeight: 1,
|
||||
letterSpacing: '-0.04em',
|
||||
}}
|
||||
>
|
||||
{feat.number}
|
||||
</div>
|
||||
|
||||
{/* Tag */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 8,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: feat.color,
|
||||
background: 'transparent',
|
||||
border: `1px solid ${feat.color}`,
|
||||
padding: '2px 7px',
|
||||
width: 'fit-content',
|
||||
opacity: 0.75,
|
||||
}}
|
||||
>
|
||||
{feat.tag}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
letterSpacing: '-0.02em',
|
||||
color: '#161C22',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{feat.title}
|
||||
</div>
|
||||
|
||||
{/* Desc */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: 13,
|
||||
color: '#475569',
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{feat.desc}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
231
marketing-video/src/scenes/Scene05Social.tsx
Normal file
231
marketing-video/src/scenes/Scene05Social.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
|
||||
// Scene 05: Social Proof — 810–960 (5s)
|
||||
// Testimonials + KPI stats
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
quote: 'Cut our expense reporting from 4 hours to 8 minutes. Absolute game-changer.',
|
||||
name: 'Sarah K.',
|
||||
role: 'CFO, TechStart GmbH',
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
quote: 'My accountant was blown away. DATEV export works flawlessly, zero corrections needed.',
|
||||
name: 'Marcus T.',
|
||||
role: 'Freelance Designer',
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
quote: 'Finally — a tool that handles German receipts perfectly. MwSt. parsing is spot-on.',
|
||||
name: 'Jana B.',
|
||||
role: 'Steuerberaterin',
|
||||
rating: 5,
|
||||
},
|
||||
];
|
||||
|
||||
const STATS = [
|
||||
{ value: '99%', label: 'AI Accuracy Rate' },
|
||||
{ value: '3s', label: 'Avg. Processing Time' },
|
||||
{ value: '50k+', label: 'Receipts Processed' },
|
||||
{ value: '€0', label: 'Manual Entry Cost' },
|
||||
];
|
||||
|
||||
export const Scene05Social: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
|
||||
const exitFade = interpolate(frame, [135, 155], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: '#000000', opacity: sceneFade * exitFade }}>
|
||||
{/* Subtle grid on dark bg */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px)',
|
||||
backgroundSize: '80px 80px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ position: 'absolute', top: 60, left: 80, right: 80 }}>
|
||||
<Reveal delay={5}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#059669',
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 26, height: 1, background: '#059669' }} />
|
||||
Trusted by thousands
|
||||
</div>
|
||||
</Reveal>
|
||||
<AnimatedHeadline
|
||||
text="Don't take our word for it."
|
||||
delay={12}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 58,
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: 1.05,
|
||||
color: '#FFFFFF',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 230,
|
||||
left: 80,
|
||||
right: 80,
|
||||
display: 'flex',
|
||||
gap: 0,
|
||||
}}
|
||||
>
|
||||
{STATS.map((stat, i) => {
|
||||
const statSpring = spring({ frame: frame - (20 + i * 12), fps, config: { damping: 16, stiffness: 110, mass: 0.7 } });
|
||||
const statOpacity = interpolate(frame - (20 + i * 12), [0, 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
const statY = interpolate(statSpring, [0, 1], [24, 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '28px 32px',
|
||||
borderRight: i < 3 ? '1px solid rgba(255,255,255,0.08)' : 'none',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
opacity: statOpacity,
|
||||
transform: `translateY(${statY}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 52,
|
||||
letterSpacing: '-0.04em',
|
||||
color: '#FFFFFF',
|
||||
lineHeight: 1,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#475569',
|
||||
}}
|
||||
>
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Testimonials row */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 430,
|
||||
left: 80,
|
||||
right: 80,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{TESTIMONIALS.map((t, i) => {
|
||||
const cardSpring = spring({ frame: frame - (55 + i * 18), fps, config: { damping: 16, stiffness: 100, mass: 0.8 } });
|
||||
const cardOpacity = interpolate(frame - (55 + i * 18), [0, 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
const cardY = interpolate(cardSpring, [0, 1], [30, 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
opacity: cardOpacity,
|
||||
transform: `translateY(${cardY}px)`,
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
padding: '24px 26px',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{/* Stars */}
|
||||
<div style={{ display: 'flex', gap: 3 }}>
|
||||
{Array.from({ length: t.rating }).map((_, si) => (
|
||||
<span key={si} style={{ color: '#FBBF24', fontSize: 14 }}>★</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quote */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: 15,
|
||||
color: '#E2E8F0',
|
||||
lineHeight: 1.6,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
"{t.quote}"
|
||||
</div>
|
||||
|
||||
{/* Author */}
|
||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
color: '#FFFFFF',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 9,
|
||||
color: '#475569',
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
{t.role}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
230
marketing-video/src/scenes/Scene06CTA.tsx
Normal file
230
marketing-video/src/scenes/Scene06CTA.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React from 'react';
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
|
||||
import { AnimatedHeadline } from '../components/AnimatedHeadline';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
|
||||
// Scene 06: CTA — 960–1050 (3s)
|
||||
// Big, punchy final call to action
|
||||
|
||||
export const Scene06CTA: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const sceneFade = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
|
||||
|
||||
// Pulsing button glow
|
||||
const glowIntensity = interpolate(
|
||||
Math.sin((frame / 15) * Math.PI),
|
||||
[-1, 1],
|
||||
[0.5, 1]
|
||||
);
|
||||
|
||||
// URL reveal progress
|
||||
const urlProgress = interpolate(frame, [50, 80], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
// Background white sweep from left
|
||||
const sweepWidth = interpolate(frame, [0, 25], [0, 100], { extrapolateRight: 'clamp' });
|
||||
|
||||
// Final logo fade
|
||||
const logoOpacity = interpolate(frame, [20, 45], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
|
||||
|
||||
const url = 'scanreceipts.app';
|
||||
const visibleChars = Math.floor(urlProgress * url.length);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
opacity: sceneFade,
|
||||
background: '#FFFFFF',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Grid bg */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '-10%',
|
||||
backgroundImage:
|
||||
'linear-gradient(#E2E8F0 1px, transparent 1px), linear-gradient(90deg, #E2E8F0 1px, transparent 1px)',
|
||||
backgroundSize: '80px 80px',
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Radial highlight */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'radial-gradient(ellipse 70% 60% at 50% 50%, rgba(5,150,105,0.06) 0%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ position: 'relative', zIndex: 1, textAlign: 'center', maxWidth: 900 }}>
|
||||
{/* Logo */}
|
||||
<div
|
||||
style={{
|
||||
opacity: logoOpacity,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 14,
|
||||
marginBottom: 40,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
border: '2.5px solid #000000',
|
||||
background: '#FFFFFF',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 17,
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
SR
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 24,
|
||||
letterSpacing: '-0.03em',
|
||||
color: '#000000',
|
||||
}}
|
||||
>
|
||||
ScanReceipts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main CTA headline */}
|
||||
<AnimatedHeadline
|
||||
text="Stop wasting hours."
|
||||
delay={18}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 90,
|
||||
letterSpacing: '-0.045em',
|
||||
lineHeight: 1.0,
|
||||
color: '#000000',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
/>
|
||||
<AnimatedHeadline
|
||||
text="Start in 30 seconds."
|
||||
delay={28}
|
||||
style={{
|
||||
fontFamily: "'Hanken Grotesk', sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 90,
|
||||
letterSpacing: '-0.045em',
|
||||
lineHeight: 1.0,
|
||||
color: '#059669',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 44,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Reveal delay={42} direction="scale">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#000000',
|
||||
color: '#FFFFFF',
|
||||
padding: '20px 52px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 15,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
boxShadow: `0 0 ${40 * glowIntensity}px ${12 * glowIntensity}px rgba(5,150,105,${0.15 * glowIntensity})`,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 18 }}>→</span>
|
||||
Try Free — No Credit Card
|
||||
</div>
|
||||
|
||||
{/* URL typewriter */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
color: '#64748B',
|
||||
letterSpacing: '0.06em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span style={{ opacity: 0.4 }}>https://</span>
|
||||
<span style={{ color: '#000000', fontWeight: 600 }}>
|
||||
{url.slice(0, visibleChars)}
|
||||
</span>
|
||||
{urlProgress < 1 && (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 2,
|
||||
height: 14,
|
||||
background: '#000000',
|
||||
opacity: Math.sin(frame * 0.3) > 0 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Badges row */}
|
||||
<Reveal delay={60}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: 28,
|
||||
marginTop: 36,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#64748B',
|
||||
borderTop: '1px solid #E2E8F0',
|
||||
paddingTop: 24,
|
||||
}}
|
||||
>
|
||||
{['✓ Free Forever Plan', '✓ GDPR Compliant', '✓ DATEV Export', '✓ No Setup Required'].map((b, i) => (
|
||||
<span key={i}>{b}</span>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
15
marketing-video/tsconfig.json
Normal file
15
marketing-video/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user