Skip to content

Commit e100929

Browse files
landing-5
1 parent 666f014 commit e100929

File tree

13 files changed

+523
-163
lines changed

13 files changed

+523
-163
lines changed

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@hypr/ui": "workspace:*",
1515
"@hypr/utils": "workspace:*",
1616
"@iconify-icon/react": "^3.0.1",
17+
"@mux/mux-player-react": "^3.7.0",
1718
"@nangohq/frontend": "^0.69.5",
1819
"@nangohq/node": "^0.69.5",
1920
"@sentry/tanstackstart-react": "^10.22.0",
File renamed without changes.
File renamed without changes.

apps/web/public/icons/yc_stone.svg

Lines changed: 11 additions & 0 deletions
Loading

apps/web/public/static.gif

4.51 MB
Loading
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { cn } from "@hypr/utils";
2+
import { Icon } from "@iconify-icon/react";
3+
import MuxPlayer, { type MuxPlayerRefAttributes } from "@mux/mux-player-react";
4+
import { useEffect, useRef } from "react";
5+
6+
interface VideoModalProps {
7+
playbackId: string;
8+
isOpen: boolean;
9+
onClose: () => void;
10+
}
11+
12+
export function VideoModal({ playbackId, isOpen, onClose }: VideoModalProps) {
13+
const playerRef = useRef<MuxPlayerRefAttributes>(null);
14+
const modalRef = useRef<HTMLDivElement>(null);
15+
16+
useEffect(() => {
17+
if (isOpen && playerRef.current) {
18+
playerRef.current.play().catch(() => {
19+
// Ignore autoplay errors
20+
});
21+
}
22+
}, [isOpen]);
23+
24+
useEffect(() => {
25+
const handleEscape = (e: KeyboardEvent) => {
26+
if (e.key === "Escape" && isOpen) {
27+
onClose();
28+
}
29+
};
30+
31+
document.addEventListener("keydown", handleEscape);
32+
return () => document.removeEventListener("keydown", handleEscape);
33+
}, [isOpen, onClose]);
34+
35+
useEffect(() => {
36+
if (isOpen) {
37+
document.body.style.overflow = "hidden";
38+
} else {
39+
document.body.style.overflow = "";
40+
}
41+
return () => {
42+
document.body.style.overflow = "";
43+
};
44+
}, [isOpen]);
45+
46+
if (!isOpen) {
47+
return null;
48+
}
49+
50+
return (
51+
<div
52+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4"
53+
onClick={onClose}
54+
>
55+
<div
56+
ref={modalRef}
57+
className="relative w-full max-w-6xl aspect-video"
58+
onClick={(e) => e.stopPropagation()}
59+
>
60+
{/* Close button */}
61+
<button
62+
onClick={onClose}
63+
className={cn(
64+
"absolute -top-12 right-0 p-2",
65+
"text-white hover:text-neutral-300",
66+
"transition-colors duration-200",
67+
)}
68+
aria-label="Close video"
69+
>
70+
<Icon icon="mdi:close" className="text-3xl" />
71+
</button>
72+
73+
{/* Video */}
74+
<MuxPlayer
75+
ref={playerRef}
76+
playbackId={playbackId}
77+
autoPlay
78+
loop
79+
accentColor="#78716c"
80+
className="w-full h-full rounded-lg"
81+
metadata={{
82+
videoTitle: "Hyprnote Feature Demo",
83+
}}
84+
/>
85+
</div>
86+
</div>
87+
);
88+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { cn } from "@hypr/utils";
2+
import MuxPlayer, { type MuxPlayerRefAttributes } from "@mux/mux-player-react";
3+
import { useEffect, useRef, useState } from "react";
4+
5+
interface VideoPlayerProps {
6+
playbackId: string;
7+
className?: string;
8+
onLearnMore?: () => void;
9+
showButtons?: boolean;
10+
onExpandVideo?: () => void;
11+
}
12+
13+
export function VideoPlayer({
14+
playbackId,
15+
className,
16+
onLearnMore,
17+
showButtons = true,
18+
onExpandVideo,
19+
}: VideoPlayerProps) {
20+
const [isHovered, setIsHovered] = useState(false);
21+
const [showControls, setShowControls] = useState(false);
22+
const playerRef = useRef<MuxPlayerRefAttributes>(null);
23+
24+
useEffect(() => {
25+
if (playerRef.current) {
26+
if (isHovered) {
27+
playerRef.current.play().catch(() => {
28+
// Ignore autoplay errors
29+
});
30+
setShowControls(true);
31+
} else {
32+
playerRef.current.pause();
33+
playerRef.current.currentTime = 0;
34+
setShowControls(false);
35+
}
36+
}
37+
}, [isHovered]);
38+
39+
return (
40+
<div
41+
className={cn("relative w-full h-full overflow-hidden group", className)}
42+
onMouseEnter={() => setIsHovered(true)}
43+
onMouseLeave={() => setIsHovered(false)}
44+
>
45+
<MuxPlayer
46+
ref={playerRef}
47+
playbackId={playbackId}
48+
loop
49+
muted
50+
playsInline
51+
accentColor="#78716c"
52+
className="w-full h-full object-cover"
53+
style={{
54+
"--controls": "none",
55+
aspectRatio: "16/9",
56+
} as React.CSSProperties}
57+
/>
58+
59+
{/* Buttons overlay - slide up from bottom */}
60+
{showButtons && showControls && (
61+
<div
62+
className={cn(
63+
"absolute bottom-0 left-0 right-0",
64+
"transition-all duration-300 ease-out",
65+
"flex gap-0",
66+
isHovered ? "translate-y-0 opacity-100" : "translate-y-full opacity-0",
67+
)}
68+
>
69+
<button
70+
onClick={(e) => {
71+
e.stopPropagation();
72+
onLearnMore?.();
73+
}}
74+
className={cn(
75+
"flex-1 py-4 text-xs font-mono",
76+
"bg-stone-100/95 text-stone-800",
77+
"border-r border-stone-400/50",
78+
"hover:bg-stone-200/95 active:bg-stone-400/95",
79+
"transition-all duration-150",
80+
"backdrop-blur-sm",
81+
)}
82+
>
83+
Learn more
84+
</button>
85+
<button
86+
onClick={(e) => {
87+
e.stopPropagation();
88+
onExpandVideo?.();
89+
}}
90+
className={cn(
91+
"flex-1 py-4 text-xs font-mono",
92+
"bg-stone-100/95 text-stone-800",
93+
"hover:bg-stone-200/95 active:bg-stone-400/95",
94+
"transition-all duration-150",
95+
"backdrop-blur-sm",
96+
)}
97+
>
98+
Expand video
99+
</button>
100+
</div>
101+
)}
102+
</div>
103+
);
104+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { cn } from "@hypr/utils";
2+
import { Icon } from "@iconify-icon/react";
3+
import MuxPlayer from "@mux/mux-player-react";
4+
5+
interface VideoThumbnailProps {
6+
playbackId: string;
7+
className?: string;
8+
onPlay?: () => void;
9+
}
10+
11+
export function VideoThumbnail({
12+
playbackId,
13+
className,
14+
onPlay,
15+
}: VideoThumbnailProps) {
16+
return (
17+
<div className={cn("relative w-full h-full overflow-hidden group cursor-pointer", className)} onClick={onPlay}>
18+
{/* Static thumbnail from Mux */}
19+
<MuxPlayer
20+
playbackId={playbackId}
21+
muted
22+
playsInline
23+
className="w-full h-full object-cover pointer-events-none"
24+
style={{
25+
"--controls": "none",
26+
aspectRatio: "16/9",
27+
} as React.CSSProperties}
28+
/>
29+
30+
{/* Overlay with play button */}
31+
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/30 transition-all duration-200 flex items-center justify-center">
32+
<button
33+
onClick={(e) => {
34+
e.stopPropagation();
35+
onPlay?.();
36+
}}
37+
className={cn(
38+
"size-16 rounded-full bg-white/90 backdrop-blur-sm",
39+
"flex items-center justify-center",
40+
"hover:bg-white hover:scale-110 transition-all duration-200",
41+
"shadow-xl",
42+
)}
43+
aria-label="Play video"
44+
>
45+
<Icon icon="mdi:play" className="text-4xl text-stone-700 ml-1" />
46+
</button>
47+
</div>
48+
</div>
49+
);
50+
}

0 commit comments

Comments
 (0)