diff --git a/public/images/landing_videos/Whisper Money - Dark - Encryption.mp4 b/public/images/landing_videos/Whisper Money - Dark - Encryption.mp4 new file mode 100644 index 00000000..6bbd1449 Binary files /dev/null and b/public/images/landing_videos/Whisper Money - Dark - Encryption.mp4 differ diff --git a/public/images/landing_videos/Whisper Money - Light - Encryption.mp4 b/public/images/landing_videos/Whisper Money - Light - Encryption.mp4 new file mode 100644 index 00000000..2766f4b6 Binary files /dev/null and b/public/images/landing_videos/Whisper Money - Light - Encryption.mp4 differ diff --git a/resources/js/components/landing/encryption-video-player.tsx b/resources/js/components/landing/encryption-video-player.tsx new file mode 100644 index 00000000..ec749792 --- /dev/null +++ b/resources/js/components/landing/encryption-video-player.tsx @@ -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(null); + const darkVideoRef = useRef(null); + const containerRef = useRef(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 ( +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > +
+
+ + {/* Replay button - visible on hover when video has ended */} + {hasEnded && ( + + )} +
+ ); +} diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 349321fc..5ad8ab51 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -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({

-
-
-
- -
+
+
+

Your Private Key

@@ -357,32 +359,36 @@ export default function Welcome({

-
-
- -
-

- Client-Side Encryption -

-

- Your transactions, accounts, and budgets - are encrypted on your device before - syncing to the cloud. -

-
- -
-
- -
-

- Zero-Knowledge Architecture -

-

- We store encrypted data we can't read. - Even if our servers were compromised, - your data stays secure. -

+
+ {[ + { + 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) => ( +
+
+ +
+

+ {item.title} +

+

+ {item.description} +

+
+ ))}