landing: add encryption video

This commit is contained in:
Víctor Falcón 2025-12-30 15:31:52 +01:00
parent 2eccee09a2
commit 1e3de4b1e0
4 changed files with 152 additions and 32 deletions

View File

@ -0,0 +1,114 @@
import { cn } from '@/lib/utils';
import { RotateCcwIcon } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
interface EncryptionVideoPlayerProps {
lightSrc: string;
darkSrc: string;
className?: string;
}
export default function EncryptionVideoPlayer({
lightSrc,
darkSrc,
className,
}: EncryptionVideoPlayerProps) {
const lightVideoRef = useRef<HTMLVideoElement>(null);
const darkVideoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hasEnded, setHasEnded] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const hasPlayedRef = useRef(false);
const handleReplay = useCallback(() => {
const lightVideo = lightVideoRef.current;
const darkVideo = darkVideoRef.current;
if (lightVideo) {
lightVideo.currentTime = 0;
lightVideo.play();
}
if (darkVideo) {
darkVideo.currentTime = 0;
darkVideo.play();
}
setHasEnded(false);
}, []);
const handleEnded = useCallback(() => {
setHasEnded(true);
}, []);
useEffect(() => {
const container = containerRef.current;
const lightVideo = lightVideoRef.current;
const darkVideo = darkVideoRef.current;
if (!container || !lightVideo || !darkVideo) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasPlayedRef.current) {
hasPlayedRef.current = true;
lightVideo.play();
darkVideo.play();
}
});
},
{
threshold: 0.5,
},
);
observer.observe(container);
return () => {
observer.disconnect();
};
}, []);
return (
<div
ref={containerRef}
className={cn('relative', className)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div className="rounded-lg border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#0a0a0a]">
<video
ref={lightVideoRef}
src={lightSrc}
muted
playsInline
onEnded={handleEnded}
className="h-full w-full rounded-lg dark:hidden"
/>
<video
ref={darkVideoRef}
src={darkSrc}
muted
playsInline
onEnded={handleEnded}
className="hidden h-full w-full rounded-lg dark:block"
/>
</div>
{/* Replay button - visible on hover when video has ended */}
{hasEnded && (
<button
onClick={handleReplay}
className={cn(
'absolute inset-0 z-20 flex cursor-pointer items-center justify-center rounded-2xl bg-black/30 transition-opacity duration-200 dark:bg-black/50',
isHovering ? 'opacity-100' : 'opacity-0',
)}
aria-label="Replay video"
>
<div className="flex size-16 items-center justify-center rounded-full bg-white/90 shadow-lg transition-transform hover:scale-110 dark:bg-[#161615]/90">
<RotateCcwIcon className="size-7 text-[#1b1b18] dark:text-[#EDEDEC]" />
</div>
</button>
)}
</div>
);
}

View File

@ -1,3 +1,4 @@
import EncryptionVideoPlayer from '@/components/landing/encryption-video-player';
import Header from '@/components/partials/header';
import { Button } from '@/components/ui/button';
import {
@ -20,7 +21,6 @@ import {
CodeIcon,
EyeOffIcon,
FileUpIcon,
KeyIcon,
LockIcon,
PieChartIcon,
ShieldCheckIcon,
@ -342,11 +342,13 @@ export default function Welcome({
</p>
</div>
<div className="grid w-full gap-8 sm:grid-cols-3">
<div className="flex flex-col items-center gap-4 rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-8 text-center dark:border-[#3E3E3A] dark:bg-[#161615]">
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10">
<KeyIcon className="size-8 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex flex-col gap-8 sm:flex-row">
<div className="flex w-full grow flex-col items-center gap-4 rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-6 text-center dark:border-[#3E3E3A] dark:bg-[#161615]">
<EncryptionVideoPlayer
lightSrc="/images/landing_videos/Whisper Money - Light - Encryption.mp4"
darkSrc="/images/landing_videos/Whisper Money - Dark - Encryption.mp4"
className="w-full max-w-4xl"
/>
<h3 className="text-xl font-semibold">
Your Private Key
</h3>
@ -357,32 +359,36 @@ export default function Welcome({
</p>
</div>
<div className="flex flex-col items-center gap-4 rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-8 text-center dark:border-[#3E3E3A] dark:bg-[#161615]">
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10">
<LockIcon className="size-8 text-emerald-600 dark:text-emerald-400" />
</div>
<h3 className="text-xl font-semibold">
Client-Side Encryption
</h3>
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
Your transactions, accounts, and budgets
are encrypted on your device before
syncing to the cloud.
</p>
</div>
<div className="flex flex-col items-center gap-4 rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-8 text-center dark:border-[#3E3E3A] dark:bg-[#161615]">
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10">
<ShieldCheckIcon className="size-8 text-emerald-600 dark:text-emerald-400" />
</div>
<h3 className="text-xl font-semibold">
Zero-Knowledge Architecture
</h3>
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
We store encrypted data we can't read.
Even if our servers were compromised,
your data stays secure.
</p>
<div className="grid grow-0 gap-8">
{[
{
icon: LockIcon,
title: 'Client-Side Encryption',
description:
'Your transactions, accounts, and budgets are encrypted on your device before syncing to the cloud.',
},
{
icon: ShieldCheckIcon,
title: 'Zero-Knowledge Architecture',
description:
"We store encrypted data we can't read. Even if our servers were compromised, your data stays secure.",
},
].map((item) => (
<div
key={item.title}
className="flex flex-col items-center justify-center gap-4 rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-6 text-center dark:border-[#3E3E3A] dark:bg-[#161615]"
>
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10">
<item.icon className="size-8 text-emerald-600 dark:text-emerald-400" />
</div>
<h3 className="text-xl font-semibold">
{item.title}
</h3>
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
{item.description}
</p>
</div>
))}
</div>
</div>
</div>