Skip to content

Commit 3bebfe3

Browse files
authored
feat: Improve Sharing Flow & Panel Resizing UX (#17)
* refactor(Playground): update panel layout logic and add arraysEqual helper - Update Playground.tsx to compare current panel layout with the default using arraysEqual - Reset layout to default when changes are detected - Add arraysEqual function in utils.ts for array comparison * improve layout and styling of the copy button * create dialog component for sharing * change to a dialog flow for sharing, and memoize shared content
1 parent 2eff4a4 commit 3bebfe3

File tree

6 files changed

+198
-29
lines changed

6 files changed

+198
-29
lines changed

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@radix-ui/react-dropdown-menu": "^2.1.4",
1717
"@radix-ui/react-progress": "^1.1.1",
1818
"@radix-ui/react-slot": "^1.1.1",
19-
"@speakeasy-api/moonshine": "^0.52.3",
19+
"@speakeasy-api/moonshine": "^0.71.0",
2020
"class-variance-authority": "^0.7.1",
2121
"clsx": "^2.1.1",
2222
"jotai": "^2.11.0",

web/pnpm-lock.yaml

Lines changed: 56 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/Playground.tsx

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { blankOverlay, petstore } from "./defaults";
1515
import speakeasyWhiteLogo from "./assets/speakeasy-white.svg";
1616
import openapiLogo from "./assets/openapi.svg";
1717
import { compress, decompress } from "@/compress";
18-
import { CopyButton } from "@/components/CopyButton";
1918
import { Button } from "@/components/ui/button";
2019
import {
2120
ImperativePanelGroupHandle,
@@ -25,7 +24,13 @@ import {
2524
} from "react-resizable-panels";
2625
import posthog from "posthog-js";
2726
import { useDebounceCallback, useMediaQuery } from "usehooks-ts";
28-
import { formatDocument, guessDocumentLanguage } from "./lib/utils";
27+
import {
28+
arraysEqual,
29+
formatDocument,
30+
guessDocumentLanguage,
31+
} from "./lib/utils";
32+
import ShareDialog, { ShareDialogHandle } from "./components/ShareDialog";
33+
import { Loader2Icon, ShareIcon } from "lucide-react";
2934

3035
const Link = ({ children, href }: { children: ReactNode; href: string }) => (
3136
<a
@@ -73,7 +78,6 @@ function Playground() {
7378
const result = useRef(blankOverlay);
7479
const [resultLoading, setResultLoading] = useState(false);
7580
const [error, setError] = useState("");
76-
const [shareUrl, setShareUrl] = useState("");
7781
const [shareUrlLoading, setShareUrlLoading] = useState(false);
7882
const [overlayMarkers, setOverlayMarkers] = useState<editor.IMarkerData[]>(
7983
[],
@@ -135,7 +139,12 @@ function Playground() {
135139

136140
const onChangeOverlayDebounced = useDebounceCallback(onChangeOverlay, 500);
137141

142+
const shareDialogRef = useRef<ShareDialogHandle>(null);
143+
const lastSharedStart = useRef<string>("");
144+
138145
const getShareUrl = useCallback(async () => {
146+
if (!shareDialogRef.current) return;
147+
139148
try {
140149
setShareUrlLoading(true);
141150
const info = await GetInfo(original.current, false);
@@ -144,14 +153,19 @@ function Playground() {
144153
original: original.current,
145154
info: info,
146155
});
147-
const blob = await compress(start);
156+
157+
const alreadySharedThis = lastSharedStart.current === start;
158+
if (alreadySharedThis) {
159+
shareDialogRef.current.setOpen(true);
160+
return;
161+
}
148162

149163
const response = await fetch("/api/share", {
150164
method: "POST",
151165
headers: {
152166
"Content-Type": "application/json",
153167
},
154-
body: blob,
168+
body: await compress(start),
155169
});
156170

157171
if (response.ok) {
@@ -161,7 +175,10 @@ function Playground() {
161175
currentUrl.hash = "";
162176
currentUrl.searchParams.set("s", base64Data);
163177

164-
setShareUrl(currentUrl.toString());
178+
lastSharedStart.current = start;
179+
shareDialogRef.current.setUrl(currentUrl.toString());
180+
shareDialogRef.current.setOpen(true);
181+
165182
history.pushState(null, "", currentUrl.toString());
166183
posthog.capture("overlay.speakeasy.com:share", {
167184
openapi: JSON.parse(info),
@@ -283,14 +300,25 @@ function Playground() {
283300

284301
const maxLayout = useCallback((index: number) => {
285302
const panelGroup = ref.current;
286-
const desiredWidths = [10, 10, 10];
287-
if (index < desiredWidths.length && index >= 0) {
288-
desiredWidths[index] = 80;
303+
if (!panelGroup) return;
304+
305+
const currentLayout = panelGroup?.getLayout();
306+
307+
if (!arraysEqual(currentLayout, defaultLayout)) {
308+
panelGroup.setLayout(defaultLayout);
309+
return;
289310
}
290-
if (panelGroup) {
291-
// Reset each Panel to 50% of the group's width
292-
panelGroup.setLayout(desiredWidths);
311+
312+
const baseWidth = 10;
313+
const maxedWidth = 80;
314+
const desiredWidths = Array(3).fill(baseWidth);
315+
316+
if (index < desiredWidths.length && index >= 0) {
317+
desiredWidths[index] = maxedWidth;
293318
}
319+
320+
// Reset each Panel to 50% of the group's width
321+
panelGroup.setLayout(desiredWidths);
294322
}, []);
295323

296324
if (!ready) {
@@ -312,7 +340,7 @@ function Playground() {
312340
For proper user experience, please use a desktop device
313341
</Alert>
314342
) : null}
315-
<div style={{ paddingBottom: "1rem", width: "100vw" }}>
343+
<div style={{ width: "100vw" }}>
316344
<div className="border-b border-muted p-4 md:p-6 text-left">
317345
<div className="flex gap-2">
318346
<div className="flex flex-1">
@@ -341,7 +369,7 @@ function Playground() {
341369
</div>
342370
</div>
343371
<div className="flex flex-1 flex-row-reverse">
344-
<div className="flex flex-col justify-between">
372+
<div className="flex flex-col gap-4 justify-between">
345373
<div className="flex gap-x-2">
346374
<span>
347375
<Link href="https://www.speakeasy.com?utm_source=overlay.speakeasy.com">
@@ -365,21 +393,27 @@ function Playground() {
365393
</Link>
366394
</span>
367395
</div>
368-
<div className="flex gap-x-2 justify-evenly ">
396+
<div className="flex gap-x-2 justify-end">
369397
<Button
370-
className="border-b border-transparent transition-all duration-200 hover:border-current"
398+
className="border-b border-transparent hover:border-current"
371399
style={{
372400
color: "#FBE331",
373401
backgroundColor: "#1E1E1E",
374402
}}
375403
onClick={getShareUrl}
376404
disabled={shareUrlLoading}
377405
>
406+
{shareUrlLoading ? (
407+
<Loader2Icon
408+
className="animate-spin"
409+
style={{ height: "75%" }}
410+
/>
411+
) : (
412+
<ShareIcon style={{ height: "75%" }} />
413+
)}
378414
Share
379415
</Button>
380-
<div className="flex items-center gap-x-2 grow">
381-
{shareUrl ? <CopyButton value={shareUrl} /> : null}
382-
</div>
416+
<ShareDialog ref={shareDialogRef} />
383417
</div>
384418
</div>
385419
</div>

web/src/components/CopyButton.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function CopyButton({
3434
React.useEffect(() => {
3535
setTimeout(() => {
3636
setHasCopied(false);
37-
}, 2000);
37+
}, 5000);
3838
}, [hasCopied]);
3939

4040
return (
@@ -49,9 +49,13 @@ export function CopyButton({
4949
style={{ background: "transparent" }}
5050
{...props}
5151
>
52-
<Input readOnly value={value} style={{ width: "100%" }} />
53-
<span className="flex items-center z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50 [&_svg]:h-3 [&_svg]:w-3">
54-
<span className="sr-only">Copy</span>
52+
<Input
53+
readOnly
54+
value={value}
55+
className="w-full font-mono font-semibold text-sm bg-zinc-900"
56+
/>
57+
<span className=" h-full aspect-square rounded-md flex items-center justify-center text-zinc-50 bg-zinc-900 hover:bg-zinc-700 hover:text-zinc-50 [&_svg]:h-3 [&_svg]:w-3">
58+
<span className="sr-only aspect-square">Copy</span>
5559
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
5660
</span>
5761
</Button>

web/src/components/ShareDialog.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
Button,
3+
Dialog,
4+
Heading,
5+
Separator,
6+
Stack,
7+
Text,
8+
} from "@speakeasy-api/moonshine";
9+
import { forwardRef, useImperativeHandle, useState } from "react";
10+
import { CopyButton } from "./CopyButton";
11+
12+
export interface ShareDialogHandle {
13+
setUrl: React.Dispatch<React.SetStateAction<string>>;
14+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
15+
}
16+
17+
const ShareDialog = forwardRef<ShareDialogHandle, {}>((_, ref) => {
18+
const [url, setUrl] = useState<string>("");
19+
const [open, setOpen] = useState(false);
20+
21+
useImperativeHandle(ref, () => ({
22+
setUrl,
23+
setOpen,
24+
}));
25+
26+
const handleClose = () => {
27+
setOpen(false);
28+
};
29+
30+
const handleOpenChange = (open: boolean) => {
31+
setOpen(open);
32+
};
33+
34+
return (
35+
<Dialog open={open} onOpenChange={handleOpenChange}>
36+
<Dialog.Content>
37+
<Dialog.Header>
38+
<Dialog.Title asChild>
39+
<div>
40+
<Heading variant="lg">Share</Heading>
41+
<Text muted variant="sm" className="leading-none ">
42+
Copy and paste the URL below anywhere to share this overlay
43+
session with others.
44+
</Text>
45+
</div>
46+
</Dialog.Title>
47+
</Dialog.Header>
48+
<Separator />
49+
<Stack direction="vertical" gap={10} className="my-2">
50+
<CopyButton className="w-full" value={url} />
51+
</Stack>
52+
<Separator />
53+
<Dialog.Footer>
54+
<Dialog.Close asChild>
55+
<Button onClick={handleClose}>Done</Button>
56+
</Dialog.Close>
57+
</Dialog.Footer>
58+
</Dialog.Content>
59+
</Dialog>
60+
);
61+
});
62+
ShareDialog.displayName = "ShareDialog";
63+
64+
export default ShareDialog;

web/src/lib/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,19 @@ export function formatDocument(doc: string, indentWidth: number = 2): string {
2222

2323
return doc;
2424
}
25+
26+
export function arraysEqual<T>(a: T[], b: T[]): boolean {
27+
// Check if the arrays have the same length
28+
if (a.length !== b.length) {
29+
return false;
30+
}
31+
32+
// Compare each element in the arrays
33+
for (let i = 0; i < a.length; i++) {
34+
if (a[i] !== b[i]) {
35+
return false;
36+
}
37+
}
38+
39+
return true;
40+
}

0 commit comments

Comments
 (0)