feat: add Remotion animated demos to landing page, show all pricing plans
Replace static mockups with Remotion Player animations: - SchemaDemo: 3x3 grid with spring animations, highlight, part info card - DashboardDemo: 5-scene product flow (VIN input, vehicle info, categories, schema, OEM codes) - Both support dark/light theme via isDark prop, autoplay + loop Expand pricing section from single teaser to all 4 plans with horizontal scroll on mobile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
673
apps/web/src/remotion/DashboardDemo.tsx
Normal file
673
apps/web/src/remotion/DashboardDemo.tsx
Normal file
@@ -0,0 +1,673 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
function getColors(isDark: boolean) {
|
||||
return {
|
||||
bg: isDark ? "#1a1a1a" : "#f5f5f5",
|
||||
fg: isDark ? "#fafafa" : "#0a0a0a",
|
||||
muted: isDark ? "#262626" : "#f5f5f5",
|
||||
mutedFg: isDark ? "#a3a3a3" : "#737373",
|
||||
border: isDark ? "#262626" : "#e5e5e5",
|
||||
emerald: "#10b981",
|
||||
surface: isDark ? "#1f1f1f" : "#ffffff",
|
||||
surfaceAlt: isDark ? "#171717" : "#fafafa",
|
||||
};
|
||||
}
|
||||
|
||||
const VIN_TEXT = "WVWZZZ1JZ3W...";
|
||||
const SIDEBAR_ITEMS = [
|
||||
"Dashboard",
|
||||
"Şase Arama",
|
||||
"Katalog",
|
||||
"Şemalar",
|
||||
"Ayarlar",
|
||||
];
|
||||
const CATEGORIES = ["Motor", "Şasi", "Elektrik", "Karoseri", "Fren"];
|
||||
const PARTS = [
|
||||
{ code: "1J0 820 803F", name: "Klima Kompresörü" },
|
||||
{ code: "1J0 698 151G", name: "Fren Balatası" },
|
||||
{ code: "1J0 819 031A", name: "Kalorifer Motoru" },
|
||||
];
|
||||
|
||||
// ─── Scene 1: VIN Input ─────────────────────────────────────────────────────
|
||||
|
||||
const VinInputScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
// Sidebar slides in
|
||||
const sidebarSlide = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 14, stiffness: 180 },
|
||||
});
|
||||
const sidebarX = interpolate(sidebarSlide, [0, 1], [-140, 0]);
|
||||
|
||||
// Typewriter effect starts at frame 30
|
||||
const typeStart = 30;
|
||||
const charsToShow = Math.min(
|
||||
Math.max(0, Math.floor((frame - typeStart) / 3)),
|
||||
VIN_TEXT.length,
|
||||
);
|
||||
const typedText = VIN_TEXT.slice(0, charsToShow);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ flexDirection: "row" }}>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
backgroundColor: c.surfaceAlt,
|
||||
borderRight: `1px solid ${c.border}`,
|
||||
padding: 12,
|
||||
transform: `translateX(${sidebarX}px)`,
|
||||
}}
|
||||
>
|
||||
{SIDEBAR_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 10,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: i === 1 ? c.fg : c.mutedFg,
|
||||
backgroundColor: i === 1 ? c.muted : "transparent",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, padding: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
backgroundColor: c.muted,
|
||||
borderRadius: 8,
|
||||
padding: "8px 12px",
|
||||
border: `1px solid ${c.border}`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={c.mutedFg}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 11,
|
||||
color: typedText ? c.fg : c.mutedFg,
|
||||
}}
|
||||
>
|
||||
{typedText || "Şase numarası girin..."}
|
||||
</span>
|
||||
{/* Blinking cursor */}
|
||||
{frame > typeStart && charsToShow < VIN_TEXT.length && (
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 14,
|
||||
backgroundColor: c.fg,
|
||||
opacity: Math.sin(frame * 0.3) > 0 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Scene 2: Vehicle Info ──────────────────────────────────────────────────
|
||||
|
||||
const VehicleInfoScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
const cardSpring = spring({
|
||||
frame: frame - 10,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 160 },
|
||||
});
|
||||
const translateY = interpolate(cardSpring, [0, 1], [50, 0]);
|
||||
const opacity = interpolate(cardSpring, [0, 1], [0, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ flexDirection: "row" }}>
|
||||
{/* Sidebar (static) */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
backgroundColor: c.surfaceAlt,
|
||||
borderRight: `1px solid ${c.border}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{SIDEBAR_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 10,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: i === 1 ? c.fg : c.mutedFg,
|
||||
backgroundColor: i === 1 ? c.muted : "transparent",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, padding: 20 }}>
|
||||
{/* VIN bar (filled) */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
backgroundColor: c.muted,
|
||||
borderRadius: 8,
|
||||
padding: "8px 12px",
|
||||
border: `1px solid ${c.border}`,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={c.mutedFg}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<span
|
||||
style={{ fontFamily: "monospace", fontSize: 11, color: c.fg }}
|
||||
>
|
||||
{VIN_TEXT}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Vehicle card */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: c.surface,
|
||||
border: `1px solid ${c.emerald}40`,
|
||||
borderRadius: 10,
|
||||
padding: 16,
|
||||
transform: `translateY(${translateY}px)`,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c.emerald,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: c.fg,
|
||||
}}
|
||||
>
|
||||
Volkswagen Golf
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
fontSize: 10,
|
||||
color: c.mutedFg,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
<span>2003</span>
|
||||
<span>1.6 FSI</span>
|
||||
<span>Hatchback</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Scene 3: Category Selection ────────────────────────────────────────────
|
||||
|
||||
const CategoryScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ flexDirection: "row" }}>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
backgroundColor: c.surfaceAlt,
|
||||
borderRight: `1px solid ${c.border}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{SIDEBAR_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 10,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: i === 2 ? c.fg : c.mutedFg,
|
||||
backgroundColor: i === 2 ? c.muted : "transparent",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, padding: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: c.fg,
|
||||
marginBottom: 12,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
Kategori Seçin
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{CATEGORIES.map((cat, i) => {
|
||||
const delay = i * 5;
|
||||
const itemSpring = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 14, stiffness: 200 },
|
||||
});
|
||||
const itemOpacity = interpolate(itemSpring, [0, 1], [0, 1]);
|
||||
const translateX = interpolate(itemSpring, [0, 1], [20, 0]);
|
||||
|
||||
// Highlight "Motor" after all items appear
|
||||
const isMotor = i === 0;
|
||||
const highlightProgress = isMotor
|
||||
? interpolate(frame, [50, 65], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
})
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={cat}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
fontSize: 11,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color:
|
||||
highlightProgress > 0.5 ? c.fg : c.mutedFg,
|
||||
backgroundColor:
|
||||
highlightProgress > 0.5
|
||||
? isDark
|
||||
? "#064e3b"
|
||||
: "#d1fae5"
|
||||
: c.muted,
|
||||
border: `1px solid ${highlightProgress > 0.5 ? c.emerald : c.border}`,
|
||||
opacity: itemOpacity,
|
||||
transform: `translateX(${translateX}px)`,
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Scene 4: Schema View ───────────────────────────────────────────────────
|
||||
|
||||
const SchemaViewScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
const HOTSPOTS = [
|
||||
{ x: "30%", y: "35%" },
|
||||
{ x: "60%", y: "25%" },
|
||||
{ x: "45%", y: "60%" },
|
||||
{ x: "70%", y: "55%" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ flexDirection: "row" }}>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
backgroundColor: c.surfaceAlt,
|
||||
borderRight: `1px solid ${c.border}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{SIDEBAR_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 10,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: i === 3 ? c.fg : c.mutedFg,
|
||||
backgroundColor: i === 3 ? c.muted : "transparent",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content — schema placeholder */}
|
||||
<div style={{ flex: 1, padding: 20, position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: c.fg,
|
||||
marginBottom: 12,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
Motor — Şema Görünümü
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
backgroundColor: c.muted,
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${c.border}`,
|
||||
height: "75%",
|
||||
}}
|
||||
>
|
||||
{/* Faint grid lines for schema feel */}
|
||||
{[0.25, 0.5, 0.75].map((pct) => (
|
||||
<div
|
||||
key={`h-${pct}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `${pct * 100}%`,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 1,
|
||||
backgroundColor: c.border,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{[0.25, 0.5, 0.75].map((pct) => (
|
||||
<div
|
||||
key={`v-${pct}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${pct * 100}%`,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 1,
|
||||
backgroundColor: c.border,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Hotspots */}
|
||||
{HOTSPOTS.map((spot, i) => {
|
||||
const delay = i * 15;
|
||||
const pulseSpring = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 120 },
|
||||
});
|
||||
const scale = interpolate(pulseSpring, [0, 1], [0, 1]);
|
||||
// Pulsing ring
|
||||
const ringScale = 1 + Math.sin((frame - delay) * 0.15) * 0.3;
|
||||
const ringOpacity = scale > 0.5 ? 0.3 + Math.sin((frame - delay) * 0.15) * 0.2 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: spot.x,
|
||||
top: spot.y,
|
||||
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
{/* Pulse ring */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: "50%",
|
||||
border: `2px solid ${c.emerald}`,
|
||||
opacity: ringOpacity,
|
||||
transform: `translate(-50%, -50%) scale(${ringScale})`,
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
}}
|
||||
/>
|
||||
{/* Dot */}
|
||||
<div
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c.emerald,
|
||||
border: `2px solid ${c.surface}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Scene 5: OEM Codes ─────────────────────────────────────────────────────
|
||||
|
||||
const OemCodesScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
// Fade out at the end
|
||||
const fadeOut = interpolate(frame, [70, 90], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ flexDirection: "row", opacity: fadeOut }}>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
backgroundColor: c.surfaceAlt,
|
||||
borderRight: `1px solid ${c.border}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{SIDEBAR_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 10,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: i === 2 ? c.fg : c.mutedFg,
|
||||
backgroundColor: i === 2 ? c.muted : "transparent",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, padding: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: c.fg,
|
||||
marginBottom: 12,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
OEM Parça Listesi
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{PARTS.map((part, i) => {
|
||||
const delay = i * 8;
|
||||
const slideSpring = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 14, stiffness: 180 },
|
||||
});
|
||||
const translateX = interpolate(slideSpring, [0, 1], [40, 0]);
|
||||
const opacity = interpolate(slideSpring, [0, 1], [0, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={part.code}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
backgroundColor: c.muted,
|
||||
border: `1px solid ${c.border}`,
|
||||
transform: `translateX(${translateX}px)`,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: c.fg,
|
||||
}}
|
||||
>
|
||||
{part.code}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: c.mutedFg,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
{part.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c.emerald,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Main Composition ───────────────────────────────────────────────────────
|
||||
|
||||
export const DashboardDemo: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const c = getColors(isDark);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: c.bg }}>
|
||||
<Sequence from={0} durationInFrames={90}>
|
||||
<VinInputScene isDark={isDark} />
|
||||
</Sequence>
|
||||
<Sequence from={90} durationInFrames={90}>
|
||||
<VehicleInfoScene isDark={isDark} />
|
||||
</Sequence>
|
||||
<Sequence from={180} durationInFrames={90}>
|
||||
<CategoryScene isDark={isDark} />
|
||||
</Sequence>
|
||||
<Sequence from={270} durationInFrames={90}>
|
||||
<SchemaViewScene isDark={isDark} />
|
||||
</Sequence>
|
||||
<Sequence from={360} durationInFrames={90}>
|
||||
<OemCodesScene isDark={isDark} />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
188
apps/web/src/remotion/SchemaDemo.tsx
Normal file
188
apps/web/src/remotion/SchemaDemo.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
const PARTS = ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9"];
|
||||
|
||||
function getColors(isDark: boolean) {
|
||||
return {
|
||||
bg: isDark ? "#1a1a1a" : "#f5f5f5",
|
||||
fg: isDark ? "#fafafa" : "#0a0a0a",
|
||||
muted: isDark ? "#262626" : "#f5f5f5",
|
||||
mutedFg: isDark ? "#a3a3a3" : "#737373",
|
||||
border: isDark ? "#262626" : "#e5e5e5",
|
||||
emerald: "#10b981",
|
||||
surface: isDark ? "#1f1f1f" : "#ffffff",
|
||||
};
|
||||
}
|
||||
|
||||
export const SchemaDemo: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const c = getColors(isDark);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: c.bg, padding: 24 }}>
|
||||
{/* Scene 1: Grid appears (0-60) */}
|
||||
<Sequence from={0} durationInFrames={240}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: 8,
|
||||
width: "100%",
|
||||
maxWidth: 280,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{PARTS.map((label, i) => {
|
||||
const delay = i * 3;
|
||||
const scale = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 200 },
|
||||
});
|
||||
|
||||
// Scene 2: Highlight center box (60-120)
|
||||
const isCenter = i === 4;
|
||||
const highlightProgress = isCenter
|
||||
? interpolate(frame, [60, 80], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
})
|
||||
: 0;
|
||||
|
||||
const boxScale = scale * (1 + highlightProgress * 0.08);
|
||||
const borderColor = isCenter
|
||||
? interpolate(highlightProgress, [0, 1], [0, 1]) > 0.5
|
||||
? c.emerald
|
||||
: c.border
|
||||
: c.border;
|
||||
const boxBg = isCenter
|
||||
? interpolate(highlightProgress, [0, 1], [0, 1]) > 0.5
|
||||
? isDark
|
||||
? "#064e3b"
|
||||
: "#d1fae5"
|
||||
: c.muted
|
||||
: c.muted;
|
||||
const textColor = isCenter && highlightProgress > 0.5 ? c.fg : c.mutedFg;
|
||||
|
||||
// Scene 4: Fade out (180-240)
|
||||
const fadeOut = interpolate(frame, [200, 240], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
aspectRatio: "1",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 8,
|
||||
border: `1.5px solid ${borderColor}`,
|
||||
backgroundColor: boxBg,
|
||||
fontSize: 12,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
color: textColor,
|
||||
transform: `scale(${boxScale})`,
|
||||
opacity: fadeOut,
|
||||
}}
|
||||
>
|
||||
{isCenter && highlightProgress > 0.5 ? "▶" : label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Sequence>
|
||||
|
||||
{/* Scene 3: Part info card (120-180) */}
|
||||
<Sequence from={0} durationInFrames={240}>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
top: "auto",
|
||||
bottom: 20,
|
||||
left: 24,
|
||||
right: 24,
|
||||
height: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const slideUp = spring({
|
||||
frame: frame - 120,
|
||||
fps,
|
||||
config: { damping: 14, stiffness: 180 },
|
||||
});
|
||||
const translateY = interpolate(slideUp, [0, 1], [40, 0]);
|
||||
const cardOpacity = interpolate(frame, [120, 135], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const fadeOut = interpolate(frame, [200, 240], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: c.surface,
|
||||
border: `1px solid ${c.border}`,
|
||||
borderRadius: 10,
|
||||
padding: "10px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
opacity: cardOpacity * fadeOut,
|
||||
transform: `translateY(${translateY}px)`,
|
||||
maxWidth: 260,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c.emerald,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontFamily: "monospace",
|
||||
color: c.fg,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
1J0 820 803F
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: c.mutedFg,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
Klima Kompresörü
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,9 @@ import {
|
||||
Gift,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Player } from "@remotion/player";
|
||||
import { SchemaDemo } from "@/remotion/SchemaDemo";
|
||||
import { DashboardDemo } from "@/remotion/DashboardDemo";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
|
||||
// ─── DATA ────────────────────────────────────────────────────────────────────
|
||||
@@ -107,22 +110,18 @@ const FEATURES = [
|
||||
"Alt kategori derinliği",
|
||||
],
|
||||
mockupTitle: "İnteraktif Şema — Motor Bölgesi",
|
||||
mockupContent: (
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex aspect-square items-center justify-center rounded-lg border border-border text-xs text-muted-foreground ${i === 4 ? "border-foreground/30 bg-muted text-foreground" : "bg-muted/50"}`}
|
||||
>
|
||||
{i === 4 ? "▶" : `P${i + 1}`}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 text-center text-xs text-muted-foreground">
|
||||
Parça seçmek için tıklayın
|
||||
</div>
|
||||
</div>
|
||||
mockupContent: (isDark: boolean) => (
|
||||
<Player
|
||||
component={SchemaDemo}
|
||||
compositionWidth={400}
|
||||
compositionHeight={300}
|
||||
durationInFrames={240}
|
||||
fps={30}
|
||||
loop
|
||||
autoPlay
|
||||
style={{ width: "100%" }}
|
||||
inputProps={{ isDark }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -705,7 +704,9 @@ function HomePage() {
|
||||
{/* Browser mockup */}
|
||||
<div className="flex-1">
|
||||
<BrowserFrame title={feature.mockupTitle}>
|
||||
{feature.mockupContent}
|
||||
{typeof feature.mockupContent === "function"
|
||||
? feature.mockupContent(isDark)
|
||||
: feature.mockupContent}
|
||||
</BrowserFrame>
|
||||
</div>
|
||||
</div>
|
||||
@@ -929,35 +930,17 @@ function HomePage() {
|
||||
|
||||
<div className="relative mt-12">
|
||||
<BrowserFrame title="sase.tr/dashboard">
|
||||
<div className="flex min-h-[300px] sm:min-h-[400px]">
|
||||
{/* Sidebar mock */}
|
||||
<div className="hidden w-48 border-r border-border p-4 sm:block">
|
||||
<div className="space-y-2">
|
||||
{["Dashboard", "Şase Arama", "Katalog", "Şemalar", "Ayarlar"].map(
|
||||
(item) => (
|
||||
<div
|
||||
key={item}
|
||||
className={`rounded-md px-3 py-1.5 text-xs ${item === "Şase Arama" ? "bg-muted text-foreground" : "text-muted-foreground"}`}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content grid mock */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-[4/3] rounded-lg bg-border"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Player
|
||||
component={DashboardDemo}
|
||||
compositionWidth={960}
|
||||
compositionHeight={540}
|
||||
durationInFrames={450}
|
||||
fps={30}
|
||||
loop
|
||||
autoPlay
|
||||
style={{ width: "100%" }}
|
||||
inputProps={{ isDark }}
|
||||
/>
|
||||
</BrowserFrame>
|
||||
|
||||
{/* Floating labels */}
|
||||
@@ -980,61 +963,108 @@ function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── 10. PRICING TEASER (YENİ) ────────────────────────────────── */}
|
||||
{/* ─── 10. PRICING ─────────────────────────────────────────────── */}
|
||||
<section className="px-4 py-24 sm:px-6">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<SectionBadge>Fiyatlandırma</SectionBadge>
|
||||
<h2 className="mt-4 font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Günde 7 TL'den Başlayan Fiyatlar
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-xl text-muted-foreground">
|
||||
Tek bir yanlış parça iadesinin maliyetinden daha az.
|
||||
</p>
|
||||
|
||||
{/* Popular plan card */}
|
||||
<div className="mx-auto mt-12 max-w-sm overflow-hidden rounded-2xl border-2 border-foreground/20 bg-surface p-8">
|
||||
<div className="mb-2 inline-flex rounded-full bg-foreground px-3 py-1 text-xs font-medium text-background">
|
||||
En Popüler
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-display)] text-xl font-bold">
|
||||
2 Marka
|
||||
</h3>
|
||||
<div className="mt-3">
|
||||
<span className="font-[family-name:var(--font-display)] text-4xl font-bold">
|
||||
350 TL
|
||||
</span>
|
||||
<span className="text-muted-foreground">/ay</span>
|
||||
</div>
|
||||
<ul className="mt-6 space-y-2 text-left text-sm">
|
||||
{[
|
||||
"2 marka seçimi",
|
||||
"Sınırsız şase arama",
|
||||
"Parça kataloğu & şema",
|
||||
"Öncelikli destek",
|
||||
].map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="size-4 shrink-0 text-emerald-500" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link to="/register">
|
||||
<Button className="mt-6 w-full rounded-full bg-foreground text-background hover:bg-foreground/90">
|
||||
7 Gün Ücretsiz Deneyin
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center">
|
||||
<SectionBadge>Fiyatlandırma</SectionBadge>
|
||||
<h2 className="mt-4 font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Günde 7 TL'den Başlayan Fiyatlar
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-xl text-muted-foreground">
|
||||
Tek bir yanlış parça iadesinin maliyetinden daha az. Tüm fiyatlar KDV dahildir.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="mt-6 inline-flex items-center gap-1 text-sm text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Tüm planları gör
|
||||
<ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
{/* Horizontally scrollable on mobile, grid on desktop */}
|
||||
<div className="mt-12 flex gap-4 overflow-x-auto pb-4 sm:pb-0 lg:grid lg:grid-cols-4 lg:overflow-visible">
|
||||
{[
|
||||
{
|
||||
name: "1 Marka",
|
||||
description: "Tek marka için yedek parça erişimi",
|
||||
price: "200",
|
||||
yearly: "2.000",
|
||||
features: ["1 marka seçimi", "Sınırsız VIN arama", "Parça kataloğu", "Şema görüntüleyici"],
|
||||
},
|
||||
{
|
||||
name: "2 Marka",
|
||||
description: "İki farklı marka için erişim",
|
||||
price: "350",
|
||||
yearly: "3.500",
|
||||
popular: true,
|
||||
features: ["2 marka seçimi", "Sınırsız VIN arama", "Parça kataloğu", "Şema görüntüleyici", "Öncelikli destek"],
|
||||
},
|
||||
{
|
||||
name: "3 Marka",
|
||||
description: "Üç marka için kapsamlı erişim",
|
||||
price: "500",
|
||||
yearly: "5.000",
|
||||
features: ["3 marka seçimi", "Sınırsız VIN arama", "Parça kataloğu", "Şema görüntüleyici", "Öncelikli destek"],
|
||||
},
|
||||
{
|
||||
name: "Full Paket",
|
||||
description: "Tüm markalara sınırsız erişim",
|
||||
price: "999",
|
||||
yearly: "9.990",
|
||||
features: ["Tüm markalar", "Sınırsız VIN arama", "Parça kataloğu", "Şema görüntüleyici", "Öncelikli destek", "OEM parça arama"],
|
||||
},
|
||||
].map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={`flex min-w-[260px] flex-1 flex-col rounded-2xl border bg-surface p-6 sm:p-8 ${plan.popular ? "border-2 border-foreground/20" : "border-border"}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="mb-3 inline-flex self-start rounded-full bg-foreground px-3 py-1 text-xs font-medium text-background">
|
||||
En Popüler
|
||||
</div>
|
||||
)}
|
||||
<h3 className="font-[family-name:var(--font-display)] text-xl font-bold">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{plan.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<span className="font-[family-name:var(--font-display)] text-4xl font-bold">
|
||||
{plan.price} TL
|
||||
</span>
|
||||
<span className="text-muted-foreground">/ay</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
veya {plan.yearly} TL/yıl
|
||||
</p>
|
||||
<ul className="mt-6 flex-1 space-y-2 text-sm">
|
||||
{plan.features.map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="size-4 shrink-0 text-emerald-500" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link to="/register" className="mt-6">
|
||||
<Button
|
||||
className={`w-full rounded-full ${plan.popular ? "bg-foreground text-background hover:bg-foreground/90" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"}`}
|
||||
variant={plan.popular ? "default" : "outline"}
|
||||
>
|
||||
7 Gün Ücretsiz Deneyin
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Detaylı karşılaştırma
|
||||
<ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user