diff --git a/web/package.json b/web/package.json index 9322c33..1e8c78e 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", - "@speakeasy-api/moonshine": "^0.52.3", + "@speakeasy-api/moonshine": "^0.71.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "jotai": "^2.11.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d0ffc4e..6c57ec8 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.18)(react@18.3.1) '@speakeasy-api/moonshine': - specifier: ^0.52.3 - version: 0.52.3(@types/react-dom@18.2.22)(@types/react@18.3.18)(lucide-react@0.453.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.1(@types/node@22.10.5)(typescript@5.2.2)) + specifier: ^0.71.0 + version: 0.71.0(@types/react-dom@18.2.22)(@types/react@18.3.18)(lucide-react@0.453.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.1(@types/node@22.10.5)(typescript@5.2.2)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -280,6 +280,28 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@edge-runtime/format@2.2.1': resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} engines: {node: '>=16'} @@ -1263,8 +1285,8 @@ packages: cpu: [x64] os: [win32] - '@speakeasy-api/moonshine@0.52.3': - resolution: {integrity: sha512-8Bk3qWyLUT4BrCOsY4VBVsiDd2ZnD3QLglzCVzkBu7jSHiCMdXgUBI/xqyZzNM2Z1T+iBWeEA4w1Qt3mJSaJQA==} + '@speakeasy-api/moonshine@0.71.0': + resolution: {integrity: sha512-31guvhZoUIjy7k52XJrqhe3fREhZ6KH3SQKKmo04StMo/JlSQiCZEAD9+buD/9IoX0vAEMNEGkArPAzYIeErFg==} peerDependencies: '@types/react': ^18.3.11 lucide-react: ^0.453.0 @@ -3934,6 +3956,31 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.6.2 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.6.2 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.6.2 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.6.2 + '@edge-runtime/format@2.2.1': {} '@edge-runtime/node-utils@2.3.0': {} @@ -4800,8 +4847,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.17.2': optional: true - '@speakeasy-api/moonshine@0.52.3(@types/react-dom@18.2.22)(@types/react@18.3.18)(lucide-react@0.453.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.1(@types/node@22.10.5)(typescript@5.2.2))': + '@speakeasy-api/moonshine@0.71.0(@types/react-dom@18.2.22)(@types/react@18.3.18)(lucide-react@0.453.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.1(@types/node@22.10.5)(typescript@5.2.2))': dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) '@radix-ui/react-collapsible': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.2.22)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.1.4(@types/react-dom@18.2.22)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4820,6 +4870,7 @@ snapshots: lucide-react: 0.453.0(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-resizable-panels: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-virtuoso: 4.12.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: 2.6.0 tailwindcss: 3.4.17(ts-node@10.9.1(@types/node@22.10.5)(typescript@5.2.2)) diff --git a/web/src/Playground.tsx b/web/src/Playground.tsx index 01b008b..a4c11fe 100644 --- a/web/src/Playground.tsx +++ b/web/src/Playground.tsx @@ -15,7 +15,6 @@ import { blankOverlay, petstore } from "./defaults"; import speakeasyWhiteLogo from "./assets/speakeasy-white.svg"; import openapiLogo from "./assets/openapi.svg"; import { compress, decompress } from "@/compress"; -import { CopyButton } from "@/components/CopyButton"; import { Button } from "@/components/ui/button"; import { ImperativePanelGroupHandle, @@ -25,7 +24,13 @@ import { } from "react-resizable-panels"; import posthog from "posthog-js"; import { useDebounceCallback, useMediaQuery } from "usehooks-ts"; -import { formatDocument, guessDocumentLanguage } from "./lib/utils"; +import { + arraysEqual, + formatDocument, + guessDocumentLanguage, +} from "./lib/utils"; +import ShareDialog, { ShareDialogHandle } from "./components/ShareDialog"; +import { Loader2Icon, ShareIcon } from "lucide-react"; const Link = ({ children, href }: { children: ReactNode; href: string }) => ( ( [], @@ -135,7 +139,12 @@ function Playground() { const onChangeOverlayDebounced = useDebounceCallback(onChangeOverlay, 500); + const shareDialogRef = useRef(null); + const lastSharedStart = useRef(""); + const getShareUrl = useCallback(async () => { + if (!shareDialogRef.current) return; + try { setShareUrlLoading(true); const info = await GetInfo(original.current, false); @@ -144,14 +153,19 @@ function Playground() { original: original.current, info: info, }); - const blob = await compress(start); + + const alreadySharedThis = lastSharedStart.current === start; + if (alreadySharedThis) { + shareDialogRef.current.setOpen(true); + return; + } const response = await fetch("/api/share", { method: "POST", headers: { "Content-Type": "application/json", }, - body: blob, + body: await compress(start), }); if (response.ok) { @@ -161,7 +175,10 @@ function Playground() { currentUrl.hash = ""; currentUrl.searchParams.set("s", base64Data); - setShareUrl(currentUrl.toString()); + lastSharedStart.current = start; + shareDialogRef.current.setUrl(currentUrl.toString()); + shareDialogRef.current.setOpen(true); + history.pushState(null, "", currentUrl.toString()); posthog.capture("overlay.speakeasy.com:share", { openapi: JSON.parse(info), @@ -283,14 +300,25 @@ function Playground() { const maxLayout = useCallback((index: number) => { const panelGroup = ref.current; - const desiredWidths = [10, 10, 10]; - if (index < desiredWidths.length && index >= 0) { - desiredWidths[index] = 80; + if (!panelGroup) return; + + const currentLayout = panelGroup?.getLayout(); + + if (!arraysEqual(currentLayout, defaultLayout)) { + panelGroup.setLayout(defaultLayout); + return; } - if (panelGroup) { - // Reset each Panel to 50% of the group's width - panelGroup.setLayout(desiredWidths); + + const baseWidth = 10; + const maxedWidth = 80; + const desiredWidths = Array(3).fill(baseWidth); + + if (index < desiredWidths.length && index >= 0) { + desiredWidths[index] = maxedWidth; } + + // Reset each Panel to 50% of the group's width + panelGroup.setLayout(desiredWidths); }, []); if (!ready) { @@ -312,7 +340,7 @@ function Playground() { For proper user experience, please use a desktop device ) : null} -
+
@@ -341,7 +369,7 @@ function Playground() {
-
+
@@ -365,9 +393,9 @@ function Playground() {
-
+
-
- {shareUrl ? : null} -
+
diff --git a/web/src/components/CopyButton.tsx b/web/src/components/CopyButton.tsx index 6678a8b..41b9714 100644 --- a/web/src/components/CopyButton.tsx +++ b/web/src/components/CopyButton.tsx @@ -34,7 +34,7 @@ export function CopyButton({ React.useEffect(() => { setTimeout(() => { setHasCopied(false); - }, 2000); + }, 5000); }, [hasCopied]); return ( @@ -49,9 +49,13 @@ export function CopyButton({ style={{ background: "transparent" }} {...props} > - - - Copy + + + Copy {hasCopied ? : } diff --git a/web/src/components/ShareDialog.tsx b/web/src/components/ShareDialog.tsx new file mode 100644 index 0000000..572fc0d --- /dev/null +++ b/web/src/components/ShareDialog.tsx @@ -0,0 +1,64 @@ +import { + Button, + Dialog, + Heading, + Separator, + Stack, + Text, +} from "@speakeasy-api/moonshine"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { CopyButton } from "./CopyButton"; + +export interface ShareDialogHandle { + setUrl: React.Dispatch>; + setOpen: React.Dispatch>; +} + +const ShareDialog = forwardRef((_, ref) => { + const [url, setUrl] = useState(""); + const [open, setOpen] = useState(false); + + useImperativeHandle(ref, () => ({ + setUrl, + setOpen, + })); + + const handleClose = () => { + setOpen(false); + }; + + const handleOpenChange = (open: boolean) => { + setOpen(open); + }; + + return ( + + + + +
+ Share + + Copy and paste the URL below anywhere to share this overlay + session with others. + +
+
+
+ + + + + + + + + + +
+
+ ); +}); +ShareDialog.displayName = "ShareDialog"; + +export default ShareDialog; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index de52093..c6185d4 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -22,3 +22,19 @@ export function formatDocument(doc: string, indentWidth: number = 2): string { return doc; } + +export function arraysEqual(a: T[], b: T[]): boolean { + // Check if the arrays have the same length + if (a.length !== b.length) { + return false; + } + + // Compare each element in the arrays + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +}