diff --git a/crates/web/frontend/docker-compose.yml b/crates/web/frontend/docker-compose.yml index a4433aa8..7d353318 100644 --- a/crates/web/frontend/docker-compose.yml +++ b/crates/web/frontend/docker-compose.yml @@ -7,6 +7,7 @@ services: - 3001:3000 environment: DATABASE_URL: postgres://postgres:password@db:5432/db + RUST_LOG: debug depends_on: db: condition: service_healthy diff --git a/crates/web/frontend/next.config.mjs b/crates/web/frontend/next.config.mjs index 4975e837..63f91c39 100644 --- a/crates/web/frontend/next.config.mjs +++ b/crates/web/frontend/next.config.mjs @@ -1,11 +1,23 @@ +const production = process.env.NODE_ENV === "production"; + const nextConfig = { - webpack: (config) => { - config.experiments = { - asyncWebAssembly: true, - layers: true, - syncWebAssembly: true, - }; - return config; - }, + async rewrites() { + return production + ? [] + : [ + { + source: "/api/:path*", + destination: "http://localhost:3001/:path*", + }, + ]; + }, + webpack: (config) => { + config.experiments = { + asyncWebAssembly: true, + layers: true, + syncWebAssembly: true, + }; + return config; + }, }; export default nextConfig; diff --git a/crates/web/frontend/package-lock.json b/crates/web/frontend/package-lock.json index f7673017..eb51fbf7 100644 --- a/crates/web/frontend/package-lock.json +++ b/crates/web/frontend/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slider": "^1.2.1", @@ -30,6 +31,7 @@ "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", "@types/js-beautify": "^1.14.3", + "@types/uuid": "^10.0.0", "@uiw/codemirror-themes": "^4.23.5", "@uiw/react-codemirror": "^4.23.5", "@wasm-tool/wasm-pack-plugin": "^1.7.0", @@ -48,9 +50,11 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", + "uuid": "^10.0.0", "vaul": "^1.0.0", "vscode-languageserver-protocol": "^3.17.5", - "webworker-promise": "^0.5.1" + "webworker-promise": "^0.5.1", + "zod": "^3.23.8" }, "devDependencies": { "@biomejs/biome": "1.9.3", @@ -1464,6 +1468,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.1.tgz", + "integrity": "sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -1983,6 +2019,12 @@ "@types/react": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@uiw/codemirror-extensions-basic-setup": { "version": "4.23.5", "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.5.tgz", @@ -5082,6 +5124,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vaul": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.0.0.tgz", @@ -5400,6 +5455,15 @@ "node": ">= 14" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "pkg": { "name": "gq-web", "version": "0.1.0", diff --git a/crates/web/frontend/package.json b/crates/web/frontend/package.json index aa23b9f5..c3fa25a4 100644 --- a/crates/web/frontend/package.json +++ b/crates/web/frontend/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slider": "^1.2.1", @@ -33,6 +34,7 @@ "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", "@types/js-beautify": "^1.14.3", + "@types/uuid": "^10.0.0", "@uiw/codemirror-themes": "^4.23.5", "@uiw/react-codemirror": "^4.23.5", "@wasm-tool/wasm-pack-plugin": "^1.7.0", @@ -51,9 +53,11 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", + "uuid": "^10.0.0", "vaul": "^1.0.0", "vscode-languageserver-protocol": "^3.17.5", - "webworker-promise": "^0.5.1" + "webworker-promise": "^0.5.1", + "zod": "^3.23.8" }, "devDependencies": { "@biomejs/biome": "1.9.3", diff --git a/crates/web/frontend/src/app/layout.tsx b/crates/web/frontend/src/app/layout.tsx index b3bc94a4..ed141482 100644 --- a/crates/web/frontend/src/app/layout.tsx +++ b/crates/web/frontend/src/app/layout.tsx @@ -6,6 +6,7 @@ import type { Metadata } from "next"; import { Fira_Mono, Montserrat } from "next/font/google"; import { Toaster } from "sonner"; import "./globals.css"; +import { TooltipProvider } from "@/components/ui/tooltip"; const montserrat = Montserrat({ subsets: ["latin"], variable: "--font-sans" }); const firaCode = Fira_Mono({ @@ -55,7 +56,9 @@ export default function RootLayout({ disableTransitionOnChange > - {children} + + {children} + diff --git a/crates/web/frontend/src/app/page-utils.ts b/crates/web/frontend/src/app/page-utils.ts index 9f85dca0..a8ba13dd 100644 --- a/crates/web/frontend/src/app/page-utils.ts +++ b/crates/web/frontend/src/app/page-utils.ts @@ -1,8 +1,9 @@ +import { notify } from "@/lib/notify"; import type { Completion } from "@/model/completion"; -import type { Data } from "@/model/data"; -import type FileType from "@/model/file-type"; +import { Data } from "@/model/data"; +import FileType from "@/model/file-type"; +import { getShare } from "@/services/shares/share-service"; import type { CompletionContext, CompletionSource } from "@codemirror/autocomplete"; -import { toast } from "sonner"; import type PromiseWorker from "webworker-promise"; export const applyGq = async ( @@ -19,7 +20,7 @@ export const applyGq = async ( outputType: outputType, indent: indent, }); - !silent && toast.success(`Query applied to ${inputData.type.toUpperCase()}`); + !silent && notify.success(`Query applied to ${inputData.type.toUpperCase()}`); return result; }; @@ -51,3 +52,21 @@ export const getQueryCompletionSource = ( }; }; }; + +export const importShare = async ( + shareId: string, +): Promise<{ input: Data; query: Data; outputType: FileType } | undefined> => { + const toastId = notify.loading("Importing share..."); + try { + const share = await getShare(shareId); + notify.success("Share successfully imported", { id: toastId }); + return { + input: new Data(share.inputContent, share.inputType), + query: new Data(share.queryContent, FileType.GQ), + outputType: share.outputType, + }; + } catch (error) { + notify.error(`Error importing share: ${error.message}`, { id: toastId }); + return undefined; + } +}; diff --git a/crates/web/frontend/src/app/page.tsx b/crates/web/frontend/src/app/page.tsx index 42c2630b..58c7f1da 100644 --- a/crates/web/frontend/src/app/page.tsx +++ b/crates/web/frontend/src/app/page.tsx @@ -6,6 +6,7 @@ import Editor from "@/components/editor/editor"; import Footer from "@/components/footer/footer"; import Header from "@/components/header/header"; import useDebounce from "@/hooks/useDebounce"; +import { notify } from "@/lib/notify"; import { cn, i } from "@/lib/utils"; import { Data } from "@/model/data"; import FileType from "@/model/file-type"; @@ -15,13 +16,56 @@ import { useSettings } from "@/providers/settings-provider"; import { useWorker } from "@/providers/worker-provider"; import type { CompletionSource } from "@codemirror/autocomplete"; import { Link2, Link2Off } from "lucide-react"; -import { useCallback, useRef, useState } from "react"; -import { toast } from "sonner"; -import { applyGq, getQueryCompletionSource } from "./page-utils"; +import { useSearchParams } from "next/navigation"; +import { type MutableRefObject, Suspense, useCallback, useEffect, useRef, useState } from "react"; +import type PromiseWorker from "webworker-promise"; +import { applyGq, getQueryCompletionSource, importShare } from "./page-utils"; import styles from "./page.module.css"; +const ShareLoader = ({ + updateInputEditorCallback, + updateQueryEditorCallback, + updateOutputData, + gqWorker, + setLinkEditors, +}: { + updateInputEditorCallback: MutableRefObject<(data: Data) => void>; + updateQueryEditorCallback: MutableRefObject<(data: Data) => void>; + updateOutputData: ( + inputContent: string, + inputType: FileType, + queryContent: string, + silent?: boolean, + outputTypeOverride?: FileType, + ) => void; + gqWorker: PromiseWorker | undefined; + setLinkEditors: (value: boolean) => void; +}) => { + const shareId = useSearchParams().get("id"); + + // biome-ignore lint/correctness/useExhaustiveDependencies: One time load when gqWorker is ready + useEffect(() => { + if (!shareId || !gqWorker) return; + importShare(shareId).then((data) => { + if (!data) return; + updateInputEditorCallback?.current(data.input); + updateQueryEditorCallback?.current(data.query); + if (data.input.type !== data.outputType) setLinkEditors(false); + updateOutputData( + data.input.content, + data.input.type, + data.query.content, + true, + data.outputType, + ); + }); + }, [gqWorker]); + + return null; +}; + const Home = () => { - const [errorMessage, setErrorMessage] = useState(undefined); + const [errorMessage, setErrorMessage] = useState(); const [warningMessages, setWarningMessages] = useState([]); const inputContent = useRef(""); const queryContent = useRef(""); @@ -35,6 +79,7 @@ const Home = () => { const updateOutputEditorCallback = useRef<(data: Data) => void>(i); const [queryCompletionSource, setQueryCompletionSource] = useState(); const [isApplying, setIsApplying] = useState(false); + const [shareLink, setShareLink] = useState(); const { settings: { autoApplySettings: { autoApply, debounceTime }, @@ -47,7 +92,13 @@ const Home = () => { const { gqWorker, lspWorker } = useWorker(); const updateOutputData = useCallback( - async (inputContent: string, inputType: FileType, queryContent: string, silent = true) => { + async ( + inputContent: string, + inputType: FileType, + queryContent: string, + silent = true, + outputTypeOverride?: FileType, + ) => { if (!gqWorker || isApplying) return; setIsApplying(true); outputEditorLoadingCallback.current( @@ -58,7 +109,7 @@ const Home = () => { const result = await applyGq( data, queryContent, - outputType.current, + outputTypeOverride || outputType.current, dataTabSize, gqWorker, silent, @@ -81,30 +132,36 @@ const Home = () => { updateInputEditorCallback.current(json); updateQueryEditorCallback.current(query); updateOutputData(json.content, json.type, query.content, true); - toast.success("Example loaded!"); + notify.success("Example loaded!"); }, [updateOutputData], ); const handleChangeInputDataFileType = useCallback( - (fileType: FileType) => linkEditors && convertOutputEditorCallback.current(fileType), + (fileType: FileType) => { + setShareLink(undefined); + linkEditors && convertOutputEditorCallback.current(fileType); + }, [linkEditors], ); const handleChangeOutputDataFileType = useCallback( - (fileType: FileType) => linkEditors && convertInputEditorCallback.current(fileType), + (fileType: FileType) => { + setShareLink(undefined); + linkEditors && convertInputEditorCallback.current(fileType); + }, [linkEditors], ); const handleChangeLinked = useCallback(() => { setSettings((prev) => setLinkEditors(prev, !linkEditors)); - toast.info(`${linkEditors ? "Unlinked" : "Linked"} editors!`); - if (linkEditors) return; - convertOutputEditorCallback.current(inputType.current); + notify.info(`${linkEditors ? "Unlinked" : "Linked"} editors!`); + if (!linkEditors) convertOutputEditorCallback.current(inputType.current); }, [linkEditors, setSettings]); const handleChangeInputContent = useCallback( (content: string) => { + setShareLink(undefined); setQueryCompletionSource(() => getQueryCompletionSource(lspWorker, new Data(content, inputType.current)), ); @@ -117,17 +174,28 @@ const Home = () => { ); const handleChangeQueryContent = useCallback( - (content: string) => + (content: string) => { + setShareLink(undefined); autoApply && - debounce(() => - updateOutputData(inputContent.current, inputType.current, content, debounceTime < 500), - ), + debounce(() => + updateOutputData(inputContent.current, inputType.current, content, debounceTime < 500), + ); + }, [autoApply, debounce, updateOutputData, debounceTime], ); return (
-
+
+ + setSettings((prev) => setLinkEditors(prev, value))} + /> +
); }; diff --git a/crates/web/frontend/src/components/action-button/action-button.tsx b/crates/web/frontend/src/components/action-button/action-button.tsx index 9bfd05b8..f8796564 100644 --- a/crates/web/frontend/src/components/action-button/action-button.tsx +++ b/crates/web/frontend/src/components/action-button/action-button.tsx @@ -1,7 +1,7 @@ import { Button, type ButtonProps } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import React from "react"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; interface Props extends ButtonProps { description: string; @@ -27,25 +27,23 @@ const ActionButton = React.forwardRef( ) => { return ( !hidden && ( - - - - - - - {description} - - - + + + + + + {description} + + ) ); }, diff --git a/crates/web/frontend/src/components/editor/editor-menu.tsx b/crates/web/frontend/src/components/editor/editor-menu.tsx index 6d532d2c..2b833b7f 100644 --- a/crates/web/frontend/src/components/editor/editor-menu.tsx +++ b/crates/web/frontend/src/components/editor/editor-menu.tsx @@ -1,7 +1,5 @@ -"use client"; - import ActionButton from "@/components/action-button/action-button"; -import ExportButton from "@/components/export-popup/export-popup"; +import ExportPopover from "@/components/export-popover/export-popover"; import ImportPopup from "@/components/import-popup/import-popup"; import type { Data } from "@/model/data"; import type FileType from "@/model/file-type"; @@ -58,7 +56,7 @@ const EditorMenu = ({ hidden={!editable} onError={onError} /> - { a.download = `${filename}.${data.type}`; a.click(); URL.revokeObjectURL(url); - toast.success("File exported succesfully!"); -}; - -export const copyToClipboard = (content: string) => { - navigator.clipboard.writeText(content); - toast.success("Copied to your clipboard!"); + notify.success("File exported successfully!"); }; export const formatCode = async ( @@ -46,13 +41,13 @@ export const formatCode = async ( formatWorker: PromiseWorker, silent = true, ): Promise => { - const toastId = silent ? undefined : toast.loading("Formatting code..."); + const toastId = silent ? undefined : notify.loading("Formatting code..."); try { const response: Data = await formatWorker.postMessage({ data, indent }); - !silent && toast.success("Code formatted!", { id: toastId }); + !silent && notify.success("Code formatted!", { id: toastId }); return response; } catch (err) { - !silent && toast.error(err.message, { id: toastId, duration: 5000 }); + !silent && notify.error(err.message, { id: toastId }); throw err; } }; @@ -64,13 +59,13 @@ export const convertCode = async ( convertWorker: PromiseWorker, silent = true, ): Promise => { - const toastId = silent ? undefined : toast.loading("Converting code..."); + const toastId = silent ? undefined : notify.loading("Converting code..."); try { const result: Data = await convertWorker.postMessage({ data, outputType, indent }); - !silent && toast.success("Code converted!", { id: toastId }); + !silent && notify.success("Code converted!", { id: toastId }); return result; } catch (err) { - !silent && toast.error(err.message, { id: toastId, duration: 5000 }); + !silent && notify.error(err.message, { id: toastId }); throw err; } }; diff --git a/crates/web/frontend/src/components/editor/editor.tsx b/crates/web/frontend/src/components/editor/editor.tsx index 0653dfcd..21df738b 100644 --- a/crates/web/frontend/src/components/editor/editor.tsx +++ b/crates/web/frontend/src/components/editor/editor.tsx @@ -1,6 +1,7 @@ import useLazyState from "@/hooks/useLazyState"; +import { MAX_RENDER_SIZE, STATE_DEBOUNCE_TIME } from "@/lib/constants"; import { gqTheme } from "@/lib/theme"; -import { cn, isMac } from "@/lib/utils"; +import { cn, copyToClipboard, isMac } from "@/lib/utils"; import { Data } from "@/model/data"; import FileType from "@/model/file-type"; import { type LoadingState, loading, notLoading } from "@/model/loading-state"; @@ -20,7 +21,6 @@ import EditorTitle from "./editor-title"; import { EditorTooLarge } from "./editor-too-large"; import { convertCode, - copyToClipboard, exportFile, formatCode, getCodemirrorExtensionsByFileType, @@ -65,7 +65,11 @@ const Editor = ({ editable = true, }: Props) => { const [editorErrorMessage, setEditorErrorMessage] = useState(); - const [content, setContent, instantContent] = useLazyState("" as string, 50, onChangeContent); + const [content, setContent, instantContent] = useLazyState( + "" as string, + STATE_DEBOUNCE_TIME, + onChangeContent, + ); const [type, setType] = useState(fileTypes[0]); const [showConsole, setShowConsole] = useState(false); const [loadingState, setLoadingState] = useState(notLoading()); @@ -77,7 +81,7 @@ const Editor = ({ } = useSettings(); const { formatWorker, convertWorker } = useWorker(); const indentSize = type === FileType.GQ ? queryTabSize : dataTabSize; - const available = content.length < 100000000; + const available = content.length < MAX_RENDER_SIZE; // const borderRepeatDelay = Math.random() * 5 + 15; const handleFormatCode = useCallback( @@ -156,6 +160,7 @@ const Editor = ({ } if (updateCallback) { updateCallback.current = (data: Data) => { + console.log("update callback", data); setContent(data.content); setType(data.type); }; diff --git a/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx b/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx index 0861205c..d2807cac 100644 --- a/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx +++ b/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx @@ -1,5 +1,3 @@ -"use client"; - import { gqTheme } from "@/lib/theme"; import { Data } from "@/model/data"; import FileType from "@/model/file-type"; diff --git a/crates/web/frontend/src/components/export-popup/export-popup.tsx b/crates/web/frontend/src/components/export-popover/export-popover.tsx similarity index 60% rename from crates/web/frontend/src/components/export-popup/export-popup.tsx rename to crates/web/frontend/src/components/export-popover/export-popover.tsx index 49f4ae0e..d9e9a611 100644 --- a/crates/web/frontend/src/components/export-popup/export-popup.tsx +++ b/crates/web/frontend/src/components/export-popover/export-popover.tsx @@ -1,13 +1,19 @@ import ActionButton from "@/components/action-button/action-button"; import type FileType from "@/model/file-type"; import { getFileExtensions } from "@/model/file-type"; -import { DialogTrigger } from "@radix-ui/react-dialog"; import { DownloadCloud } from "lucide-react"; import { type FormEvent, useState } from "react"; import { Button } from "../ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "../ui/popover"; interface Props { defaultFilename: string; @@ -15,7 +21,7 @@ interface Props { onExportFile: (fileName: string) => void; } -const ExportPopup = ({ defaultFilename, fileType, onExportFile }: Props) => { +const ExportPopover = ({ defaultFilename, fileType, onExportFile }: Props) => { const [fileName, setFileName] = useState(defaultFilename); const [open, setOpen] = useState(false); @@ -26,20 +32,20 @@ const ExportPopup = ({ defaultFilename, fileType, onExportFile }: Props) => { }; return ( - - + + - - - - Export to file - + + + + Export to file + Export the content of the editor to a file with a custom name - - -
+ + +
{
-
- - -
+
-
-
+ + ); }; -export default ExportPopup; +export default ExportPopover; diff --git a/crates/web/frontend/src/components/header/header.tsx b/crates/web/frontend/src/components/header/header.tsx index 0a8ad0bb..6f3473ae 100644 --- a/crates/web/frontend/src/components/header/header.tsx +++ b/crates/web/frontend/src/components/header/header.tsx @@ -2,17 +2,34 @@ import SettingsSheet from "@/components/settings-sheet/settings-sheet"; import ThemeButton from "@/components/theme-button/theme-button"; import { cn } from "@/lib/utils"; import type { Data } from "@/model/data"; -import { memo } from "react"; +import type FileType from "@/model/file-type"; +import { type MutableRefObject, memo } from "react"; import ExamplesSheet from "../examples-sheet/examples-sheet"; +import SharePopover from "../share-popover/share-popover"; import ShortcutPopup from "../shortcut-popup/shortcut-popup"; import StarCount from "../star-count/star-count"; interface Props { className?: string; onClickExample: (json: Data, query: Data) => void; + inputContent: MutableRefObject; + inputType: MutableRefObject; + queryContent: MutableRefObject; + outputType: MutableRefObject; + shareLink: string | undefined; + setShareLink: (shareLink?: string) => void; } -const Header = ({ className, onClickExample }: Props) => { +const Header = ({ + className, + onClickExample, + inputContent, + inputType, + queryContent, + outputType, + shareLink, + setShareLink, +}: Props) => { return (
@@ -28,6 +45,14 @@ const Header = ({ className, onClickExample }: Props) => {
+
diff --git a/crates/web/frontend/src/components/import-popup/import-popup.tsx b/crates/web/frontend/src/components/import-popup/import-popup.tsx index f90cebe8..be020b59 100644 --- a/crates/web/frontend/src/components/import-popup/import-popup.tsx +++ b/crates/web/frontend/src/components/import-popup/import-popup.tsx @@ -1,5 +1,6 @@ import ActionButton from "@/components/action-button/action-button"; import useLazyState from "@/hooks/useLazyState"; +import { STATE_DEBOUNCE_TIME } from "@/lib/constants"; import { formatBytes } from "@/lib/utils"; import { Data } from "@/model/data"; import type FileType from "@/model/file-type"; @@ -8,8 +9,8 @@ import { fromString } from "@/model/http-method"; import { type LoadingState, notLoading } from "@/model/loading-state"; import { File, FileUp, Trash } from "lucide-react"; import { type ChangeEvent, useCallback, useMemo, useState } from "react"; -import BodyTab from "../body-tab/body-tab"; -import HeadersTab from "../headers-tab/headers-tab"; +import RequestBodyTab from "../request-body-tab/request-body-tab"; +import RequestHeadersTab from "../request-headers-tab/request-headers-tab"; import { Button } from "../ui/button"; import { Dialog, @@ -53,7 +54,7 @@ const ImportPopup = ({ const [open, setOpen] = useState(false); const [httpMethod, setHttpMethod] = useState<"GET" | "POST">("GET"); const [headers, setHeaders] = useState<[string, string, boolean][]>([["", "", true]]); - const [body, setBody, instantBody] = useLazyState("", 50); + const [body, setBody, instantBody] = useLazyState("", STATE_DEBOUNCE_TIME); const [selectedUrlTab, setSelectedUrlTab] = useState<"headers" | "body">("headers"); const [url, setUrl] = useState(""); const [file, setFile] = useState(); @@ -211,11 +212,11 @@ const ImportPopup = ({ )} - + {httpMethod === "POST" && ( - + )} @@ -275,7 +276,7 @@ const ImportPopup = ({ > Cancel -
diff --git a/crates/web/frontend/src/components/import-popup/import-utils.ts b/crates/web/frontend/src/components/import-popup/import-utils.ts index 8b062cd4..4bd6b942 100644 --- a/crates/web/frontend/src/components/import-popup/import-utils.ts +++ b/crates/web/frontend/src/components/import-popup/import-utils.ts @@ -1,3 +1,4 @@ +import { notify } from "@/lib/notify"; import { statusTextMap } from "@/lib/utils"; import { Data } from "@/model/data"; import type FileType from "@/model/file-type"; @@ -25,14 +26,14 @@ export const validateFile = ( const error = new Error( `Files of type ${importedFileType} cannot be imported into this editor`, ); - toast.error(error.message); + notify.error(error.message); onError?.(error); return; } onSuccess?.({ f: file, type: importedFileType }); } catch { const error = new Error(`Unable to import files of type ${file.type}`); - toast.error(error.message); + notify.error(error.message); onError?.(error); } }; diff --git a/crates/web/frontend/src/components/body-tab/body-tab.tsx b/crates/web/frontend/src/components/request-body-tab/request-body-tab.tsx similarity index 93% rename from crates/web/frontend/src/components/body-tab/body-tab.tsx rename to crates/web/frontend/src/components/request-body-tab/request-body-tab.tsx index 89e57367..708689bd 100644 --- a/crates/web/frontend/src/components/body-tab/body-tab.tsx +++ b/crates/web/frontend/src/components/request-body-tab/request-body-tab.tsx @@ -7,12 +7,12 @@ import CodeMirror, { type Extension } from "@uiw/react-codemirror"; import { useCallback, useEffect, useMemo, useState } from "react"; import { formatCode, getCodemirrorExtensionsByFileType } from "../editor/editor-utils"; -interface BodyTabProps { +interface RequestBodyTabProps { body: string; setBody: (body: string) => void; } -const BodyTab = ({ body, setBody }: BodyTabProps) => { +const RequestBodyTab = ({ body, setBody }: RequestBodyTabProps) => { const [focused, setFocused] = useState(false); const { settings: { @@ -67,4 +67,4 @@ const BodyTab = ({ body, setBody }: BodyTabProps) => { ); }; -export default BodyTab; +export default RequestBodyTab; diff --git a/crates/web/frontend/src/components/headers-tab/headers-datalist.tsx b/crates/web/frontend/src/components/request-headers-tab/request-headers-datalist.tsx similarity index 86% rename from crates/web/frontend/src/components/headers-tab/headers-datalist.tsx rename to crates/web/frontend/src/components/request-headers-tab/request-headers-datalist.tsx index 43d2c53b..b2ed02a9 100644 --- a/crates/web/frontend/src/components/headers-tab/headers-datalist.tsx +++ b/crates/web/frontend/src/components/request-headers-tab/request-headers-datalist.tsx @@ -2,7 +2,7 @@ interface HeadersDatalistProps { id: string; } -const HeadersDatalist = ({ id }: HeadersDatalistProps) => { +const RequestHeadersDatalist = ({ id }: HeadersDatalistProps) => { return (