Skip to content

Commit 521dadb

Browse files
committed
feat: add shortcut demo video to bookmarklet dialog
1 parent db7012a commit 521dadb

File tree

6 files changed

+377
-149
lines changed

6 files changed

+377
-149
lines changed

components/app-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Link from "next/link";
22
import { Suspense } from "react";
3-
import { Bookmarklet } from "@/components/bookmarklet";
3+
import { BookmarkletDialog } from "@/components/bookmarklet-dialog";
44
import { GithubStars } from "@/components/github-stars";
55
import { HeaderSearch } from "@/components/header-search";
66
import { Logo } from "@/components/logo";
@@ -41,7 +41,7 @@ export function AppHeader() {
4141
</Suspense>
4242
{/* The bookmarklet is practially uselesss on mobile */}
4343
<Separator orientation="vertical" className="!h-4 hidden sm:block" />
44-
<Bookmarklet className="hidden sm:block" />
44+
<BookmarkletDialog className="hidden sm:block" />
4545
{/* Theme toggle is always shown */}
4646
<Separator orientation="vertical" className="!h-4" />
4747
<ThemeToggle />

components/bookmarklet.test.tsx renamed to components/bookmarklet-dialog.test.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
/* @vitest-environment jsdom */
22
import { render, screen } from "@testing-library/react";
33
import userEvent from "@testing-library/user-event";
4-
import { describe, expect, it } from "vitest";
5-
import { Bookmarklet } from "@/components/bookmarklet";
4+
import { describe, expect, it, vi } from "vitest";
65

7-
describe("Bookmarklet", () => {
8-
it("opens dialog when clicking the trigger", async () => {
9-
render(<Bookmarklet />);
10-
await userEvent.click(
11-
screen.getByRole("button", { name: /open bookmarklet info/i }),
12-
);
13-
expect(
14-
screen.getByRole("heading", { name: /bookmarklet/i }),
15-
).toBeInTheDocument();
16-
});
6+
// Mock the video player components to avoid media-chrome dependencies
7+
vi.mock("@/components/ui/video-player", () => ({
8+
VideoPlayer: ({ children }: { children: React.ReactNode }) => (
9+
<div data-testid="video-player">{children}</div>
10+
),
11+
VideoPlayerContent: () => <div data-testid="video-content" />,
12+
}));
13+
14+
import { BookmarkletDialog } from "@/components/bookmarklet-dialog";
1715

16+
describe("Bookmarklet", () => {
1817
it("sets Inspect Domain href to a javascript: url", async () => {
19-
render(<Bookmarklet />);
18+
render(<BookmarkletDialog />);
2019
await userEvent.click(
2120
screen.getByRole("button", { name: /open bookmarklet info/i }),
2221
);

components/bookmarklet.tsx renamed to components/bookmarklet-dialog.tsx

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CornerLeftUp,
66
Layers2,
77
MousePointerClick,
8+
Play,
89
} from "lucide-react";
910
import { useCallback, useEffect, useState } from "react";
1011
import { toast } from "sonner";
@@ -18,12 +19,13 @@ import {
1819
DialogTrigger,
1920
} from "@/components/ui/dialog";
2021
import { Separator } from "@/components/ui/separator";
22+
import { VideoPlayer, VideoPlayerContent } from "@/components/ui/video-player";
2123
import { cn } from "@/lib/utils";
2224

2325
// retrieve this from the last segment of the icloud.com URL provided when sharing a shortcut
2426
const APPLE_SHORTCUT_ID = "fa17677a0d6440c2a195e608305d6f2b";
2527

26-
export function Bookmarklet({ className }: { className?: string }) {
28+
export function BookmarkletDialog({ className }: { className?: string }) {
2729
// Capture the origin after mount to avoid SSR issues and ensure we have the correct origin
2830
const [origin, setOrigin] = useState<string>("");
2931

@@ -105,19 +107,59 @@ export function Bookmarklet({ className }: { className?: string }) {
105107
</span>{" "}
106108
option will now appear when you share a webpage from Safari.
107109
</p>
108-
<a
109-
// https://www.icloud.com/shortcuts/fa17677a0d6440c2a195e608305d6f2b
110-
href={`workflow://shortcuts/${APPLE_SHORTCUT_ID}`}
111-
target="_blank"
112-
rel="noopener"
113-
className={cn(
114-
buttonVariants({ variant: "outline", size: "lg" }),
115-
"!px-3",
116-
)}
117-
>
118-
<Layers2 />
119-
<span>Add Shortcut</span>
120-
</a>
110+
<div className="flex gap-2">
111+
<a
112+
// https://www.icloud.com/shortcuts/fa17677a0d6440c2a195e608305d6f2b
113+
href={`workflow://shortcuts/${APPLE_SHORTCUT_ID}`}
114+
target="_blank"
115+
rel="noopener"
116+
className={cn(
117+
buttonVariants({ variant: "outline", size: "lg" }),
118+
"!px-3",
119+
)}
120+
>
121+
<Layers2 />
122+
<span>Add Shortcut</span>
123+
</a>
124+
125+
<Dialog>
126+
<DialogTrigger asChild>
127+
<Button
128+
variant="outline"
129+
size="lg"
130+
className="!px-3"
131+
aria-label="Watch demo"
132+
>
133+
<Play />
134+
<span>Watch Demo</span>
135+
</Button>
136+
</DialogTrigger>
137+
138+
<DialogContent className="max-h-[90vh] overflow-y-auto border-border/80 dark:border-border/50">
139+
<DialogHeader className="sr-only">
140+
<DialogTitle>Shortcut Demo</DialogTitle>
141+
<DialogDescription>
142+
See how the Apple Shortcut works to inspect domains from
143+
Safari.
144+
</DialogDescription>
145+
</DialogHeader>
146+
147+
<VideoPlayer>
148+
<VideoPlayerContent
149+
className="h-full object-contain"
150+
crossOrigin=""
151+
preload="auto"
152+
slot="media"
153+
src="https://res.cloudinary.com/dhema2uls/video/upload/v1762374050/shortcut_demo_mn2egd.mp4"
154+
autoPlay
155+
muted
156+
loop
157+
playsInline
158+
/>
159+
</VideoPlayer>
160+
</DialogContent>
161+
</Dialog>
162+
</div>
121163
</div>
122164
</DialogContent>
123165
</Dialog>

components/ui/video-player.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"use client";
2+
3+
import {
4+
MediaControlBar,
5+
MediaController,
6+
MediaMuteButton,
7+
MediaPlayButton,
8+
MediaSeekBackwardButton,
9+
MediaSeekForwardButton,
10+
MediaTimeDisplay,
11+
MediaTimeRange,
12+
MediaVolumeRange,
13+
} from "media-chrome/react";
14+
import type { ComponentProps, CSSProperties } from "react";
15+
import { cn } from "@/lib/utils";
16+
17+
export type VideoPlayerProps = ComponentProps<typeof MediaController>;
18+
19+
const variables = {
20+
"--media-primary-color": "var(--primary)",
21+
"--media-secondary-color": "var(--background)",
22+
"--media-text-color": "var(--foreground)",
23+
"--media-background-color": "var(--background)",
24+
"--media-control-hover-background": "var(--accent)",
25+
"--media-font-family": "var(--font-sans)",
26+
"--media-live-button-icon-color": "var(--muted-foreground)",
27+
"--media-live-button-indicator-color": "var(--destructive)",
28+
"--media-range-track-background": "var(--border)",
29+
} as CSSProperties;
30+
31+
export const VideoPlayer = ({ style, ...props }: VideoPlayerProps) => (
32+
<MediaController
33+
style={{
34+
...variables,
35+
...style,
36+
}}
37+
{...props}
38+
/>
39+
);
40+
41+
export type VideoPlayerControlBarProps = ComponentProps<typeof MediaControlBar>;
42+
43+
export const VideoPlayerControlBar = (props: VideoPlayerControlBarProps) => (
44+
<MediaControlBar {...props} />
45+
);
46+
47+
export type VideoPlayerTimeRangeProps = ComponentProps<typeof MediaTimeRange>;
48+
49+
export const VideoPlayerTimeRange = ({
50+
className,
51+
...props
52+
}: VideoPlayerTimeRangeProps) => (
53+
<MediaTimeRange className={cn("p-2.5", className)} {...props} />
54+
);
55+
56+
export type VideoPlayerTimeDisplayProps = ComponentProps<
57+
typeof MediaTimeDisplay
58+
>;
59+
60+
export const VideoPlayerTimeDisplay = ({
61+
className,
62+
...props
63+
}: VideoPlayerTimeDisplayProps) => (
64+
<MediaTimeDisplay className={cn("p-2.5", className)} {...props} />
65+
);
66+
67+
export type VideoPlayerVolumeRangeProps = ComponentProps<
68+
typeof MediaVolumeRange
69+
>;
70+
71+
export const VideoPlayerVolumeRange = ({
72+
className,
73+
...props
74+
}: VideoPlayerVolumeRangeProps) => (
75+
<MediaVolumeRange className={cn("p-2.5", className)} {...props} />
76+
);
77+
78+
export type VideoPlayerPlayButtonProps = ComponentProps<typeof MediaPlayButton>;
79+
80+
export const VideoPlayerPlayButton = ({
81+
className,
82+
...props
83+
}: VideoPlayerPlayButtonProps) => (
84+
<MediaPlayButton className={cn("p-2.5", className)} {...props} />
85+
);
86+
87+
export type VideoPlayerSeekBackwardButtonProps = ComponentProps<
88+
typeof MediaSeekBackwardButton
89+
>;
90+
91+
export const VideoPlayerSeekBackwardButton = ({
92+
className,
93+
...props
94+
}: VideoPlayerSeekBackwardButtonProps) => (
95+
<MediaSeekBackwardButton className={cn("p-2.5", className)} {...props} />
96+
);
97+
98+
export type VideoPlayerSeekForwardButtonProps = ComponentProps<
99+
typeof MediaSeekForwardButton
100+
>;
101+
102+
export const VideoPlayerSeekForwardButton = ({
103+
className,
104+
...props
105+
}: VideoPlayerSeekForwardButtonProps) => (
106+
<MediaSeekForwardButton className={cn("p-2.5", className)} {...props} />
107+
);
108+
109+
export type VideoPlayerMuteButtonProps = ComponentProps<typeof MediaMuteButton>;
110+
111+
export const VideoPlayerMuteButton = ({
112+
className,
113+
...props
114+
}: VideoPlayerMuteButtonProps) => (
115+
<MediaMuteButton className={cn("p-2.5", className)} {...props} />
116+
);
117+
118+
export type VideoPlayerContentProps = ComponentProps<"video">;
119+
120+
export const VideoPlayerContent = ({
121+
className,
122+
...props
123+
}: VideoPlayerContentProps) => (
124+
<video className={cn("mt-0 mb-0", className)} {...props} />
125+
);

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
"@date-fns/utc": "^2.1.1",
3434
"@neondatabase/serverless": "^1.0.2",
3535
"@opentelemetry/api": "^1.9.0",
36-
"@posthog/nextjs-config": "^1.3.9",
36+
"@posthog/nextjs-config": "^1.3.10",
3737
"@readme/http-status-codes": "^9.0.5",
3838
"@sparticuz/chromium": "141.0.0",
39-
"@tanstack/react-query": "^5.90.6",
39+
"@tanstack/react-query": "^5.90.7",
4040
"@tanstack/react-query-devtools": "^5.90.2",
4141
"@trpc/client": "^11.7.1",
4242
"@trpc/server": "^11.7.1",
@@ -65,13 +65,14 @@
6565
"ipaddr.js": "^2.2.0",
6666
"lucide-react": "^0.552.0",
6767
"mapbox-gl": "^3.16.0",
68+
"media-chrome": "^4.15.1",
6869
"motion": "^12.23.24",
6970
"ms": "3.0.0-canary.202508261828",
7071
"next": "16.0.1",
7172
"next-themes": "^0.4.6",
7273
"postgres": "^3.4.7",
73-
"posthog-js": "^1.285.1",
74-
"posthog-node": "^5.11.0",
74+
"posthog-js": "^1.288.0",
75+
"posthog-node": "^5.11.1",
7576
"puppeteer-core": "24.26.1",
7677
"radix-ui": "^1.4.3",
7778
"rdapper": "^0.11.0",
@@ -89,8 +90,8 @@
8990
"zod": "^4.1.12"
9091
},
9192
"devDependencies": {
92-
"@biomejs/biome": "2.3.3",
93-
"@electric-sql/pglite": "^0.3.12",
93+
"@biomejs/biome": "2.3.4",
94+
"@electric-sql/pglite": "^0.3.13",
9495
"@tailwindcss/postcss": "^4.1.16",
9596
"@testing-library/dom": "10.4.1",
9697
"@testing-library/jest-dom": "6.9.1",

0 commit comments

Comments
 (0)