From 3425b87ec8bac849ec7f3a3050d5cacefbeb0122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <72865058+daavidrgz@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:04:29 +0100 Subject: [PATCH] feature(front): queries history in playground (#55) --- crates/web/frontend/package-lock.json | 7 + crates/web/frontend/package.json | 1 + crates/web/frontend/src/app/global-error.tsx | 24 +-- crates/web/frontend/src/app/globals.css | 12 ++ crates/web/frontend/src/app/layout.tsx | 5 +- crates/web/frontend/src/app/page.tsx | 185 ++++++++--------- .../components/apply-button/apply-button.tsx | 27 ++- .../editor-overlay/editor-overlay.module.css | 5 +- .../src/components/editor/editor-menu.tsx | 12 +- .../src/components/editor/editor-title.tsx | 2 +- .../components/editor/editor-too-large.tsx | 12 +- .../src/components/editor/editor.module.css | 10 +- .../frontend/src/components/editor/editor.tsx | 80 ++++---- .../src/components/editor/simple-editor.tsx | 4 +- .../examples-sheet/examples-sheet.module.css | 3 - .../examples-tab.tsx} | 119 ++++------- .../examples.ts | 0 .../export-popover/export-popover.tsx | 26 ++- .../frontend/src/components/footer/footer.tsx | 32 +-- .../frontend/src/components/header/header.tsx | 63 ------ .../history-tab/history-tab-utils.ts | 87 ++++++++ .../components/history-tab/history-tab.tsx | 168 ++++++++++++++++ .../components/import-popup/import-popup.tsx | 27 ++- .../components/left-sidebar/left-sidebar.tsx | 175 ++++++++++++++++ .../components/link-editor/link-editor.tsx | 23 +++ .../onboarding-popup.module.css | 2 +- .../onboarding-popup/onboarding-popup.tsx | 11 +- .../request-headers-tab.tsx | 32 +-- .../settings-sheet/settings-sheet.tsx | 150 -------------- .../components/settings-tab/settings-tab.tsx | 128 ++++++++++++ .../share-popover/share-popover.tsx | 186 ------------------ .../share-tab-utils.ts} | 0 .../src/components/share-tab/share-tab.tsx | 172 ++++++++++++++++ .../shortcut-popup/shortcut-popup.tsx | 66 ++++--- .../{shortcuts.ts => shortcuts.tsx} | 9 + .../src/components/star-count/star-count.tsx | 37 ++-- .../components/theme-button/theme-button.tsx | 19 +- .../frontend/src/components/ui/accordion.tsx | 4 +- .../src/components/ui/alert-dialog.tsx | 24 +-- .../web/frontend/src/components/ui/button.tsx | 17 +- .../web/frontend/src/components/ui/dialog.tsx | 7 +- .../src/components/ui/dropdown-menu.tsx | 10 +- .../web/frontend/src/components/ui/input.tsx | 2 +- .../frontend/src/components/ui/popover.tsx | 4 +- .../web/frontend/src/components/ui/select.tsx | 6 +- .../web/frontend/src/components/ui/sheet.tsx | 12 +- .../frontend/src/components/ui/sidebar.tsx | 52 +++++ .../frontend/src/components/ui/skeleton.tsx | 7 + .../src/components/ui/slider-with-tooltip.tsx | 28 ++- .../web/frontend/src/components/ui/slider.tsx | 2 +- .../web/frontend/src/components/ui/table.tsx | 12 +- .../web/frontend/src/components/ui/tabs.tsx | 6 +- .../frontend/src/components/ui/tooltip.tsx | 2 +- .../web-assembly-badge/web-assembly-badge.tsx | 17 ++ .../{useDebounce.tsx => use-debounce.tsx} | 8 +- .../{useLazyState.tsx => use-lazy-state.tsx} | 8 +- crates/web/frontend/src/hooks/use-mobile.tsx | 19 ++ .../web/frontend/src/hooks/use-onboarding.tsx | 22 +++ crates/web/frontend/src/lib/constants.ts | 5 + crates/web/frontend/src/lib/utils.ts | 4 + crates/web/frontend/src/model/settings.ts | 4 + crates/web/frontend/src/model/user-query.ts | 5 + .../src/services/queries/query-service.ts | 98 +++++++++ crates/web/frontend/tailwind.config.ts | 20 +- 64 files changed, 1477 insertions(+), 849 deletions(-) delete mode 100644 crates/web/frontend/src/components/examples-sheet/examples-sheet.module.css rename crates/web/frontend/src/components/{examples-sheet/examples-sheet.tsx => examples-tab/examples-tab.tsx} (61%) rename crates/web/frontend/src/components/{examples-sheet => examples-tab}/examples.ts (100%) delete mode 100644 crates/web/frontend/src/components/header/header.tsx create mode 100644 crates/web/frontend/src/components/history-tab/history-tab-utils.ts create mode 100644 crates/web/frontend/src/components/history-tab/history-tab.tsx create mode 100644 crates/web/frontend/src/components/left-sidebar/left-sidebar.tsx create mode 100644 crates/web/frontend/src/components/link-editor/link-editor.tsx delete mode 100644 crates/web/frontend/src/components/settings-sheet/settings-sheet.tsx create mode 100644 crates/web/frontend/src/components/settings-tab/settings-tab.tsx delete mode 100644 crates/web/frontend/src/components/share-popover/share-popover.tsx rename crates/web/frontend/src/components/{share-popover/share-popover-utils.ts => share-tab/share-tab-utils.ts} (100%) create mode 100644 crates/web/frontend/src/components/share-tab/share-tab.tsx rename crates/web/frontend/src/components/shortcut-popup/{shortcuts.ts => shortcuts.tsx} (75%) create mode 100644 crates/web/frontend/src/components/ui/sidebar.tsx create mode 100644 crates/web/frontend/src/components/ui/skeleton.tsx create mode 100644 crates/web/frontend/src/components/web-assembly-badge/web-assembly-badge.tsx rename crates/web/frontend/src/hooks/{useDebounce.tsx => use-debounce.tsx} (55%) rename crates/web/frontend/src/hooks/{useLazyState.tsx => use-lazy-state.tsx} (79%) create mode 100644 crates/web/frontend/src/hooks/use-mobile.tsx create mode 100644 crates/web/frontend/src/hooks/use-onboarding.tsx create mode 100644 crates/web/frontend/src/model/user-query.ts create mode 100644 crates/web/frontend/src/services/queries/query-service.ts diff --git a/crates/web/frontend/package-lock.json b/crates/web/frontend/package-lock.json index eb51fbf7..eab4be9c 100644 --- a/crates/web/frontend/package-lock.json +++ b/crates/web/frontend/package-lock.json @@ -41,6 +41,7 @@ "copy-webpack-plugin": "^12.0.2", "framer-motion": "^11.9.0", "gq-web": "file:pkg", + "idb": "^8.0.0", "lucide-react": "^0.447.0", "next": "14.2.14", "next-themes": "^0.3.0", @@ -3498,6 +3499,12 @@ "node": ">= 0.4" } }, + "node_modules/idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/crates/web/frontend/package.json b/crates/web/frontend/package.json index c3fa25a4..33564643 100644 --- a/crates/web/frontend/package.json +++ b/crates/web/frontend/package.json @@ -44,6 +44,7 @@ "copy-webpack-plugin": "^12.0.2", "framer-motion": "^11.9.0", "gq-web": "file:pkg", + "idb": "^8.0.0", "lucide-react": "^0.447.0", "next": "14.2.14", "next-themes": "^0.3.0", diff --git a/crates/web/frontend/src/app/global-error.tsx b/crates/web/frontend/src/app/global-error.tsx index 7707b711..4a747995 100644 --- a/crates/web/frontend/src/app/global-error.tsx +++ b/crates/web/frontend/src/app/global-error.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"; import { Fira_Mono, Montserrat } from "next/font/google"; import { useEffect } from "react"; import "./globals.css"; +import { deleteDatabase } from "@/services/queries/query-service"; const montserrat = Montserrat({ subsets: ["latin"], variable: "--font-sans" }); const firaCode = Fira_Mono({ @@ -21,6 +22,12 @@ export default function GlobalError({ }) { useEffect(() => console.error(error), [error]); + const handleTryAgain = async () => { + localStorage.clear(); + await deleteDatabase(); + window.location.reload(); + }; + return (

Something went wrong!

-

- Click the button below to refresh the playground state +

+ Click the button below to refresh the playground state. This will delete all your saved + queries and your settings aiming to solve the issue.

-
diff --git a/crates/web/frontend/src/app/globals.css b/crates/web/frontend/src/app/globals.css index 24784623..bf4df240 100644 --- a/crates/web/frontend/src/app/globals.css +++ b/crates/web/frontend/src/app/globals.css @@ -94,3 +94,15 @@ code { li[aria-selected="true"] { @apply !bg-accent-background !text-[var(--code-secondary)]; } + +h3 { + @apply text-lg font-semibold leading-none tracking-tight; +} + +h4 { + @apply text-base font-semibold leading-none tracking-tight; +} + +p { + @apply text-xs; +} diff --git a/crates/web/frontend/src/app/layout.tsx b/crates/web/frontend/src/app/layout.tsx index ed141482..908cd83d 100644 --- a/crates/web/frontend/src/app/layout.tsx +++ b/crates/web/frontend/src/app/layout.tsx @@ -29,7 +29,6 @@ export default function RootLayout({ { const [queryCompletionSource, setQueryCompletionSource] = useState(); const [isApplying, setIsApplying] = useState(false); const [shareLink, setShareLink] = useState(); + const [sidebarOpen, setSidebarOpen] = useState(false); + const addNewQueryCallback = useRef<(queryContent: string) => void>(i); const { settings: { autoApplySettings: { autoApply, debounceTime }, @@ -88,7 +85,7 @@ const Home = () => { }, setSettings, } = useSettings(); - const debounce = useDebounce(debounceTime); + const debounce = useDebounce(); const { gqWorker, lspWorker } = useWorker(); const updateOutputData = useCallback( @@ -114,6 +111,7 @@ const Home = () => { gqWorker, silent, ); + addNewQueryCallback.current(queryContent); setErrorMessage(undefined); updateOutputEditorCallback.current(result); } catch (err) { @@ -137,6 +135,10 @@ const Home = () => { [updateOutputData], ); + const handleClickQuery = useCallback((queryContent: string) => { + updateQueryEditorCallback.current(new Data(queryContent, FileType.GQ)); + }, []); + const handleChangeInputDataFileType = useCallback( (fileType: FileType) => { setShareLink(undefined); @@ -153,9 +155,9 @@ const Home = () => { [linkEditors], ); - const handleChangeLinked = useCallback(() => { + const handleToggleLinked = useCallback(() => { setSettings((prev) => setLinkEditors(prev, !linkEditors)); - notify.info(`${linkEditors ? "Unlinked" : "Linked"} editors!`); + notify.info(`Editors ${linkEditors ? "unlinked" : "linked"}!`); if (!linkEditors) convertOutputEditorCallback.current(inputType.current); }, [linkEditors, setSettings]); @@ -166,7 +168,7 @@ const Home = () => { getQueryCompletionSource(lspWorker, new Data(content, inputType.current)), ); autoApply && - debounce(() => + debounce(debounceTime, () => updateOutputData(content, inputType.current, queryContent.current, debounceTime < 500), ); }, @@ -177,7 +179,7 @@ const Home = () => { (content: string) => { setShareLink(undefined); autoApply && - debounce(() => + debounce(debounceTime, () => updateOutputData(inputContent.current, inputType.current, content, debounceTime < 500), ); }, @@ -185,10 +187,13 @@ const Home = () => { ); return ( -
-
+ { shareLink={shareLink} setShareLink={setShareLink} /> -
- -
-
-
- - {linkEditors ? : } - -
-
- - updateOutputData(inputContent.current, inputType.current, queryContent.current, false) - } - /> -
- -
-
- - setSettings((prev) => setLinkEditors(prev, value))} +
+
+ + +
+
- -
+ + setSettings((prev) => setLinkEditors(prev, value))} + /> + +
+ ); }; diff --git a/crates/web/frontend/src/components/apply-button/apply-button.tsx b/crates/web/frontend/src/components/apply-button/apply-button.tsx index 03db217a..39a09c8d 100644 --- a/crates/web/frontend/src/components/apply-button/apply-button.tsx +++ b/crates/web/frontend/src/components/apply-button/apply-button.tsx @@ -1,14 +1,23 @@ import ActionButton from "@/components/action-button/action-button"; -import { isMac } from "@/lib/utils"; +import { cn, isMac } from "@/lib/utils"; +import { useSettings } from "@/providers/settings-provider"; import { CirclePlay, Play } from "lucide-react"; import { useCallback, useEffect } from "react"; interface Props { - autoApply: boolean; - onClick: () => void; + onClick?: () => void; + className?: string; } -const ApplyButton = ({ autoApply, onClick }: Props) => { +const ApplyButton = ({ className, onClick }: Props) => { + if (!onClick) return null; + + const { + settings: { + autoApplySettings: { autoApply }, + }, + } = useSettings(); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { if ((isMac ? e.metaKey : e.ctrlKey) && e.key === "Enter") { @@ -27,18 +36,18 @@ const ApplyButton = ({ autoApply, onClick }: Props) => { return autoApply ? ( - + ) : ( - + ); }; diff --git a/crates/web/frontend/src/components/editor-overlay/editor-overlay.module.css b/crates/web/frontend/src/components/editor-overlay/editor-overlay.module.css index 686d5137..bc024430 100644 --- a/crates/web/frontend/src/components/editor-overlay/editor-overlay.module.css +++ b/crates/web/frontend/src/components/editor-overlay/editor-overlay.module.css @@ -41,6 +41,7 @@ position: absolute; z-index: 10; left: 0; + bottom: 0; height: 3rem; padding: 0 2rem; display: flex; @@ -48,7 +49,7 @@ justify-content: space-between; width: 100%; background-color: var(--error); - transition: bottom 0.2s ease-in-out, opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; & > span { @apply text-xs font-mono; @@ -59,12 +60,10 @@ } &[data-visible="true"] { - bottom: 0; opacity: 1; } &[data-visible="false"] { - bottom: -3rem; opacity: 0; } } diff --git a/crates/web/frontend/src/components/editor/editor-menu.tsx b/crates/web/frontend/src/components/editor/editor-menu.tsx index 2b833b7f..5c8347f0 100644 --- a/crates/web/frontend/src/components/editor/editor-menu.tsx +++ b/crates/web/frontend/src/components/editor/editor-menu.tsx @@ -1,10 +1,12 @@ import ActionButton from "@/components/action-button/action-button"; import ExportPopover from "@/components/export-popover/export-popover"; import ImportPopup from "@/components/import-popup/import-popup"; +import { cn } from "@/lib/utils"; import type { Data } from "@/model/data"; import type FileType from "@/model/file-type"; import type { LoadingState } from "@/model/loading-state"; import { Braces, Clipboard } from "lucide-react"; +import ApplyButton from "../apply-button/apply-button"; interface Props { currentType: FileType; @@ -17,6 +19,7 @@ interface Props { onExportFile: (filename: string) => void; onChangeLoading: (loading: LoadingState) => void; onError: (error: Error) => void; + onApply?: () => void; } const EditorMenu = ({ @@ -30,18 +33,19 @@ const EditorMenu = ({ onExportFile, onChangeLoading, onError, + onApply, }: Props) => { return ( -
+
); }; diff --git a/crates/web/frontend/src/components/editor/editor-title.tsx b/crates/web/frontend/src/components/editor/editor-title.tsx index 0fd4b33b..d919577a 100644 --- a/crates/web/frontend/src/components/editor/editor-title.tsx +++ b/crates/web/frontend/src/components/editor/editor-title.tsx @@ -17,7 +17,7 @@ const EditorTitle = ({ title, fileTypes, currentFileType, onChangeFileType }: Pr }, [currentFileType, fileTypes, onChangeFileType]); return ( -

+

{title} {fileTypes.length === 1 ? ( {currentFileType.toUpperCase()} diff --git a/crates/web/frontend/src/components/editor/editor-too-large.tsx b/crates/web/frontend/src/components/editor/editor-too-large.tsx index 79663490..4223493a 100644 --- a/crates/web/frontend/src/components/editor/editor-too-large.tsx +++ b/crates/web/frontend/src/components/editor/editor-too-large.tsx @@ -8,11 +8,11 @@ interface Props { export const EditorTooLarge = ({ editable, onClearContent }: Props) => { return ( -
-

The input is too large to be displayed here!

-

+

+

The input is too large to be displayed here!

+

You can still use the playground exporting the results or copying the output to your - clipboard. + clipboard

{editable && ( { onClick={onClearContent} description="Clear the input by deleting all the content" > -
- +
+ Clear input
diff --git a/crates/web/frontend/src/components/editor/editor.module.css b/crates/web/frontend/src/components/editor/editor.module.css index 177d9c9d..29bcf0f1 100644 --- a/crates/web/frontend/src/components/editor/editor.module.css +++ b/crates/web/frontend/src/components/editor/editor.module.css @@ -2,13 +2,13 @@ transition: box-shadow 0.2s, background-color 0.2s; &[data-focused="true"] { - background-color: var(--accent-subtle); - box-shadow: 0 60px 60px -100px var(--shadow-accent); + /* background-color: var(--accent-subtle); */ + /* box-shadow: 0 60px 60px -100px var(--shadow-accent); */ } &[data-focused="false"] { - background-color: var(--accent-background); - box-shadow: 0 60px 60px -100px var(--shadow); + /* background-color: var(--accent-background); */ + /* box-shadow: 0 60px 60px -100px var(--shadow); */ } } @@ -58,7 +58,7 @@ } .languageToggle { - @apply p-0 pr-2 rounded-lg flex items-center justify-center gap-2 select-none cursor-pointer relative; + @apply p-0 rounded-lg flex items-center justify-center gap-2 select-none cursor-pointer relative; & span { @apply text-lg font-bold; diff --git a/crates/web/frontend/src/components/editor/editor.tsx b/crates/web/frontend/src/components/editor/editor.tsx index 21df738b..d7059499 100644 --- a/crates/web/frontend/src/components/editor/editor.tsx +++ b/crates/web/frontend/src/components/editor/editor.tsx @@ -1,4 +1,4 @@ -import useLazyState from "@/hooks/useLazyState"; +import useLazyState from "@/hooks/use-lazy-state"; import { MAX_RENDER_SIZE, STATE_DEBOUNCE_TIME } from "@/lib/constants"; import { gqTheme } from "@/lib/theme"; import { cn, copyToClipboard, isMac } from "@/lib/utils"; @@ -9,9 +9,8 @@ import { useSettings } from "@/providers/settings-provider"; import { useWorker } from "@/providers/worker-provider"; import type { CompletionSource } from "@codemirror/autocomplete"; import CodeMirror, { type Extension } from "@uiw/react-codemirror"; -import { cubicBezier, motion } from "framer-motion"; import { TriangleAlert } from "lucide-react"; -import { type MutableRefObject, useCallback, useEffect, useMemo, useState } from "react"; +import { type MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; import ActionButton from "../action-button/action-button"; import EditorConsole from "../editor-console/editor-console"; import EditorErrorOverlay from "../editor-overlay/editor-error-overlay"; @@ -27,12 +26,13 @@ import { } from "./editor-utils"; import styles from "./editor.module.css"; -interface Props { +interface Props extends React.HTMLAttributes { title: string; defaultFileName: string; fileTypes: FileType[]; onChangeFileType?: (fileType: FileType) => void; onChangeContent?: (content: string) => void; + onApply?: () => void; className?: string; errorMessage?: string; onDismissError?: () => void; @@ -44,6 +44,8 @@ interface Props { completionSource?: CompletionSource; contentRef?: MutableRefObject; typeRef?: MutableRefObject; + width: string; + height: string; } const Editor = ({ @@ -52,6 +54,7 @@ const Editor = ({ fileTypes, onChangeFileType, onChangeContent, + onApply, className, errorMessage, onDismissError, @@ -62,7 +65,10 @@ const Editor = ({ completionSource, contentRef, typeRef, + width, + height, editable = true, + ...props }: Props) => { const [editorErrorMessage, setEditorErrorMessage] = useState(); const [content, setContent, instantContent] = useLazyState( @@ -73,7 +79,7 @@ const Editor = ({ const [type, setType] = useState(fileTypes[0]); const [showConsole, setShowConsole] = useState(false); const [loadingState, setLoadingState] = useState(notLoading()); - const [focused, onChangeFocused] = useState(false); + const focused = useRef(false); // Ref to avoid rerendering const { settings: { formattingSettings: { formatOnImport, dataTabSize, queryTabSize }, @@ -82,7 +88,6 @@ const Editor = ({ const { formatWorker, convertWorker } = useWorker(); const indentSize = type === FileType.GQ ? queryTabSize : dataTabSize; const available = content.length < MAX_RENDER_SIZE; - // const borderRepeatDelay = Math.random() * 5 + 15; const handleFormatCode = useCallback( async (cont: string, type: FileType) => { @@ -114,13 +119,13 @@ const Editor = ({ const handleKeyDown = useCallback( (event: KeyboardEvent) => { - if (!focused) return; + if (!focused.current) return; if ((isMac ? event.metaKey : event.ctrlKey) && (event.key === "s" || event.key === "S")) { event.preventDefault(); handleFormatCode(content, type); } }, - [focused, handleFormatCode, content, type], + [handleFormatCode, content, type], ); const handleChangeFileType = useCallback( @@ -160,7 +165,6 @@ const Editor = ({ } if (updateCallback) { updateCallback.current = (data: Data) => { - console.log("update callback", data); setContent(data.content); setType(data.type); }; @@ -181,9 +185,17 @@ const Editor = ({ [type, completionSource], ); + const handleChangeFocused = useCallback((value: boolean) => { + focused.current = value; + }, []); + return ( -
-
+
+
exportFile(new Data(content, type), filename)} onChangeLoading={setLoadingState} onError={(err) => setEditorErrorMessage(err.message)} + onApply={onApply} />
-
- - +
setShowConsole(false)} /> -
+
{available ? ( onChangeFocused(true)} - onBlur={() => onChangeFocused(false)} - className="w-full h-full rounded-lg text-xs overflow-hidden" + onFocus={() => handleChangeFocused(true)} + onBlur={() => handleChangeFocused(false)} + className="w-full h-full text-xs overflow-hidden" value={instantContent} onChange={setContent} height="100%" diff --git a/crates/web/frontend/src/components/editor/simple-editor.tsx b/crates/web/frontend/src/components/editor/simple-editor.tsx index 480a242a..1a00d1f5 100644 --- a/crates/web/frontend/src/components/editor/simple-editor.tsx +++ b/crates/web/frontend/src/components/editor/simple-editor.tsx @@ -7,12 +7,12 @@ interface Props extends React.HTMLAttributes { const SimpleEditor = ({ content, className, ...rest }: Props) => { return ( -
+
{content.split("\n").map((line, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: {index + 1} - {line.replaceAll("\t", "\u00a0\u00a0")} + {line.replaceAll("\t", "\u00a0\u00a0").replaceAll(" ", "\u00a0")} ))}
diff --git a/crates/web/frontend/src/components/examples-sheet/examples-sheet.module.css b/crates/web/frontend/src/components/examples-sheet/examples-sheet.module.css deleted file mode 100644 index 713fab81..00000000 --- a/crates/web/frontend/src/components/examples-sheet/examples-sheet.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.accordion > :last-child { - border-bottom: 0 !important; -} diff --git a/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx b/crates/web/frontend/src/components/examples-tab/examples-tab.tsx similarity index 61% rename from crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx rename to crates/web/frontend/src/components/examples-tab/examples-tab.tsx index d2807cac..d13c515a 100644 --- a/crates/web/frontend/src/components/examples-sheet/examples-sheet.tsx +++ b/crates/web/frontend/src/components/examples-tab/examples-tab.tsx @@ -5,12 +5,9 @@ import { useSettings } from "@/providers/settings-provider"; import { useWorker } from "@/providers/worker-provider"; import { json } from "@codemirror/lang-json"; import CodeMirror from "@uiw/react-codemirror"; -import { Book } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import ActionButton from "../action-button/action-button"; +import { useCallback, useState } from "react"; import { formatCode } from "../editor/editor-utils"; import SimpleEditor from "../editor/simple-editor"; -import OnboardingPopup from "../onboarding-popup/onboarding-popup"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion"; import { AlertDialog, @@ -22,16 +19,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "../ui/sheet"; +import { SidebarContent, SidebarDescription, SidebarHeader, SidebarTitle } from "../ui/sidebar"; import { type Example, type ExampleSection, queryExamples } from "./examples"; -import styles from "./examples-sheet.module.css"; interface ExampleItemDescriptionProps { description: string; @@ -73,17 +62,17 @@ const ExampleItemDescription = ({ description, className }: ExampleItemDescripti const ExampleItem = ({ example, onClick }: ExampleItemProps) => { return (
onClick(example.query)} onKeyDown={(event) => event.key === "Enter" && onClick(example.query)} >

{example.title}

event.stopPropagation()} content={example.query} /> @@ -104,12 +93,14 @@ const ExamplesSection = ({ title, exampleSection, onClick }: ExampleSectionProps ); return ( - - {title} + + + {title} + -
+
{ - const [onboardingVisible, setOnboardingVisible] = useState(false); - const [sheetOpen, setSheetOpen] = useState(false); +const ExamplesTab = ({ onClickExample, className }: Props) => { const [dialogOpen, setDialogOpen] = useState(false); const [selectedExample, setSelectedExample] = useState<{ json: string; @@ -151,34 +140,16 @@ const ExamplesSheet = ({ onClickExample, className }: Props) => { setDialogOpen(true); }, []); - const handleCloseOnboarding = useCallback(() => { - setOnboardingVisible(false); - localStorage.setItem("onboarding", "done"); - }, []); - - const handleOpenChange = useCallback( - (open: boolean) => { - onboardingVisible && handleCloseOnboarding(); - setSheetOpen(open); - }, - [onboardingVisible, handleCloseOnboarding], - ); - const handleSubmit = useCallback(async () => { if (!selectedExample || !formatWorker) return; const jsonData = new Data(selectedExample.json, FileType.JSON); const queryData = new Data(selectedExample.query, FileType.GQ); const formattedJson = await formatCode(jsonData, dataTabSize, formatWorker, true); const formattedQuery = await formatCode(queryData, queryTabSize, formatWorker, true); - setSheetOpen(false); setDialogOpen(false); onClickExample(formattedJson, formattedQuery); }, [dataTabSize, queryTabSize, onClickExample, selectedExample, formatWorker]); - useEffect(() => { - localStorage.getItem("onboarding") || setOnboardingVisible(true); - }, []); - return ( <> @@ -186,53 +157,43 @@ const ExamplesSheet = ({ onClickExample, className }: Props) => { Replace editor content? - This will replace the content of both json and query editors with the selected - example. + This will replace the content of both json and query editors with the selected example - - setDialogOpen(false)}>Cancel - + + setDialogOpen(false)} + > + Cancel + + Continue - - -
- - - - + + Query Examples + + Check some query examples and import them into your editor with ease. + + + + {queryExamples.map((exampleSection: ExampleSection) => ( + -
-
- - - Query Examples - - Check some query examples and import them into your editor with ease. There are - endless possiblities! - - - - {queryExamples.map((exampleSection: ExampleSection) => ( - - ))} - - -
+ ))} + + ); }; -export default ExamplesSheet; +export default ExamplesTab; diff --git a/crates/web/frontend/src/components/examples-sheet/examples.ts b/crates/web/frontend/src/components/examples-tab/examples.ts similarity index 100% rename from crates/web/frontend/src/components/examples-sheet/examples.ts rename to crates/web/frontend/src/components/examples-tab/examples.ts diff --git a/crates/web/frontend/src/components/export-popover/export-popover.tsx b/crates/web/frontend/src/components/export-popover/export-popover.tsx index d9e9a611..97ae53d8 100644 --- a/crates/web/frontend/src/components/export-popover/export-popover.tsx +++ b/crates/web/frontend/src/components/export-popover/export-popover.tsx @@ -1,4 +1,5 @@ import ActionButton from "@/components/action-button/action-button"; +import { cn } from "@/lib/utils"; import type FileType from "@/model/file-type"; import { getFileExtensions } from "@/model/file-type"; import { DownloadCloud } from "lucide-react"; @@ -19,9 +20,10 @@ interface Props { defaultFilename: string; fileType: FileType; onExportFile: (fileName: string) => void; + className?: string; } -const ExportPopover = ({ defaultFilename, fileType, onExportFile }: Props) => { +const ExportPopover = ({ defaultFilename, fileType, onExportFile, className }: Props) => { const [fileName, setFileName] = useState(defaultFilename); const [open, setOpen] = useState(false); @@ -34,19 +36,24 @@ const ExportPopover = ({ defaultFilename, fileType, onExportFile }: Props) => { return ( - + - - + + Export to file Export the content of the editor to a file with a custom name -
- + +
{ minLength={1} maxLength={255} placeholder="Enter file name" - className="w-full mt-2 rounded-r-none" + className="w-full m-0 border-x-0" value={fileName} onChange={(e) => setFileName(e.target.value)} /> - + .{getFileExtensions(fileType)[0]}
- -
diff --git a/crates/web/frontend/src/components/footer/footer.tsx b/crates/web/frontend/src/components/footer/footer.tsx index 52be86f1..85a8e0cc 100644 --- a/crates/web/frontend/src/components/footer/footer.tsx +++ b/crates/web/frontend/src/components/footer/footer.tsx @@ -1,24 +1,30 @@ import { cn } from "@/lib/utils"; -import Image from "next/image"; -import { Badge } from "../ui/badge"; +import { LinkEditor } from "../link-editor/link-editor"; +import ShortcutPopup from "../shortcut-popup/shortcut-popup"; +import StarCount from "../star-count/star-count"; +import { WebAssemblyBadge } from "../web-assembly-badge/web-assembly-badge"; interface FooterProps { + linkEditors: boolean; + handleToggleLinked: () => void; className?: string; } -const Footer = ({ className }: FooterProps) => { +const Footer = ({ linkEditors, handleToggleLinked, className }: FooterProps) => { return ( -
- - Powered by - GQ Logo +
+ + - +
+
+ + +
); }; diff --git a/crates/web/frontend/src/components/header/header.tsx b/crates/web/frontend/src/components/header/header.tsx deleted file mode 100644 index 6f3473ae..00000000 --- a/crates/web/frontend/src/components/header/header.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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 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, - inputContent, - inputType, - queryContent, - outputType, - shareLink, - setShareLink, -}: Props) => { - return ( -
-
- - - -
- -

- - GQ Playground - -

- -
- - - -
-
- ); -}; - -export default memo(Header); diff --git a/crates/web/frontend/src/components/history-tab/history-tab-utils.ts b/crates/web/frontend/src/components/history-tab/history-tab-utils.ts new file mode 100644 index 00000000..e3d54e37 --- /dev/null +++ b/crates/web/frontend/src/components/history-tab/history-tab-utils.ts @@ -0,0 +1,87 @@ +import type { UserQuery } from "@/model/user-query"; + +type GroupedQueries = { + [key: string]: UserQuery[]; +}; + +export const groupQueries = (queries: UserQuery[]): GroupedQueries => { + const groupedQueries: GroupedQueries = {}; + const now = Date.now(); + const buckets = [ + { + label: "Today", + dayDiff: 1, + }, + { + label: "Yesterday", + dayDiff: 2, + }, + { + label: "Two days ago", + dayDiff: 3, + }, + { + label: "Three days ago", + dayDiff: 4, + }, + { + label: "Four days ago", + dayDiff: 5, + }, + { + label: "Five days ago", + dayDiff: 6, + }, + { + label: "Six days ago", + dayDiff: 7, + }, + { + label: "One week ago", + dayDiff: 14, + }, + { + label: "Two weeks ago", + dayDiff: 21, + }, + { + label: "Three weeks ago", + dayDiff: 28, + }, + { + label: "One month ago", + dayDiff: 60, + }, + { + label: "Two months ago", + dayDiff: 90, + }, + { + label: "Three months ago", + dayDiff: 120, + }, + ]; + const stepsTimestamps = buckets.map((bucket) => ({ + label: bucket.label, + timestamp: now - bucket.dayDiff * 24 * 60 * 60 * 1000, + })); + stepsTimestamps.push({ + label: "A long time ago", + timestamp: 0, + }); + + let currentStep = 0; + for (const query of queries) { + while (query.timestamp < stepsTimestamps[currentStep].timestamp) { + currentStep++; + } + const step = stepsTimestamps[currentStep]; + const stepKey = step.label; + if (!groupedQueries[stepKey]) { + groupedQueries[stepKey] = []; + } + groupedQueries[stepKey].push(query); + } + + return groupedQueries; +}; diff --git a/crates/web/frontend/src/components/history-tab/history-tab.tsx b/crates/web/frontend/src/components/history-tab/history-tab.tsx new file mode 100644 index 00000000..f4dcd6de --- /dev/null +++ b/crates/web/frontend/src/components/history-tab/history-tab.tsx @@ -0,0 +1,168 @@ +import useDebounce from "@/hooks/use-debounce"; +import { HISTORY_PAGE_SIZE } from "@/lib/constants"; +import { capitalize, cn, countLines } from "@/lib/utils"; +import type { UserQuery } from "@/model/user-query"; +import { addQuery, deleteQuery, getPaginatedQueries } from "@/services/queries/query-service"; +import { AnimatePresence, motion } from "framer-motion"; +import { Redo, Trash, X } from "lucide-react"; +import { type MutableRefObject, useCallback, useEffect, useState } from "react"; +import ActionButton from "../action-button/action-button"; +import SimpleEditor from "../editor/simple-editor"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { SidebarContent, SidebarDescription, SidebarHeader, SidebarTitle } from "../ui/sidebar"; +import { groupQueries } from "./history-tab-utils"; + +interface Props { + onClickQuery: (queryContent: string) => void; + addNewQueryCallback: MutableRefObject<(queryContent: string) => void>; + className?: string; +} + +const HistoryTab = ({ onClickQuery, addNewQueryCallback, className }: Props) => { + const [search, setSearch] = useState(""); + const [currentPage, setCurrentPage] = useState(0); + const [hasMore, setHasMore] = useState(false); + const [queries, setQueries] = useState([]); + const debounce = useDebounce(); + + const handleSearch = useCallback(async (value: string) => { + setCurrentPage(0); + const [matchingQueries, hasMore] = await getPaginatedQueries(0, HISTORY_PAGE_SIZE, value); + setHasMore(hasMore); + setQueries(matchingQueries); + }, []); + + const handleAddNewQuery = useCallback( + async (content: string) => { + const [addedQuery, deletedQuery] = await addQuery(content); + const newQueries = deletedQuery + ? queries.filter((query) => query.id !== deletedQuery.id) + : queries; + addedQuery && newQueries.unshift(addedQuery); + setQueries([...newQueries]); + }, + [queries], + ); + + const handleDeleteQuery = useCallback( + async (id: number) => { + await deleteQuery(id); + setQueries(queries.filter((query) => query.id !== id)); + }, + [queries], + ); + + const handleLoadMore = useCallback(async () => { + const nextPage = currentPage + 1; + const [matchingQueries, hasMore] = await getPaginatedQueries( + nextPage, + HISTORY_PAGE_SIZE, + search, + ); + setCurrentPage(nextPage); + setHasMore(hasMore); + setQueries((prevQueries) => [...prevQueries, ...matchingQueries]); + }, [currentPage, search]); + + useEffect(() => debounce(200, () => handleSearch(search)), [search, debounce, handleSearch]); + + useEffect(() => { + addNewQueryCallback.current = handleAddNewQuery; + }, [handleAddNewQuery, addNewQueryCallback]); + + return ( + + + Query history + Check the previous queries you have made. + +
e.preventDefault()}> + setSearch(e.target.value)} + value={search} + placeholder="Type to search..." + /> + +
+ {Object.entries(groupQueries(queries)).map((entry) => ( +
+
+ {capitalize(entry[0])} +
+ + {entry[1].map((query) => ( + + +
2 + ? "group-hover:max-w-10" + : "group-hover:max-w-20 flex", + )} + > + 2 ? "right" : "bottom"} + containerClassName={cn( + "min-h-10 flex items-center justify-center border-l", + countLines(query.content) > 2 ? "h-1/2 border-b" : "h-full", + )} + className="h-full w-10 border-0" + description="Dump query into the editor" + onClick={() => onClickQuery(query.content)} + > + + + 2 ? "right" : "bottom"} + containerClassName={cn( + "min-h-10 flex items-center justify-center border-l", + countLines(query.content) > 2 ? "h-1/2" : "h-full", + )} + className="h-full w-10 border-0" + description="Delete from history" + onClick={() => handleDeleteQuery(query.id)} + > + + +
+
+ ))} +
+
+ ))} + {hasMore && ( + + )} +
+ ); +}; + +export default HistoryTab; 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 be020b59..0da855a6 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,5 @@ import ActionButton from "@/components/action-button/action-button"; -import useLazyState from "@/hooks/useLazyState"; +import useLazyState from "@/hooks/use-lazy-state"; import { STATE_DEBOUNCE_TIME } from "@/lib/constants"; import { formatBytes } from "@/lib/utils"; import { Data } from "@/model/data"; @@ -51,6 +51,8 @@ const ImportPopup = ({ onError, hidden = false, }: Props) => { + if (hidden) return null; + const [open, setOpen] = useState(false); const [httpMethod, setHttpMethod] = useState<"GET" | "POST">("GET"); const [headers, setHeaders] = useState<[string, string, boolean][]>([["", "", true]]); @@ -136,7 +138,7 @@ const ImportPopup = ({ return ( - @@ -149,7 +151,7 @@ const ImportPopup = ({
- + From URL @@ -157,10 +159,10 @@ const ImportPopup = ({ From local file - +
updateHeaders(index, e.target.value, header[1], header[2])} - className="w-1/2 p-2 border rounded-md mb-0 peer-data-[state=unchecked]:opacity-50 transition-opacity" + className="w-1/2 p-2 border mb-0 peer-data-[state=unchecked]:opacity-50 transition-opacity" /> { placeholder="Value" value={header[1]} onChange={(e) => updateHeaders(index, header[0], e.target.value, header[2])} - className="w-1/2 p-2 border rounded-md mb-0 peer-data-[state=unchecked]:opacity-50 transition-opacity" + className="w-1/2 p-2 border mb-0 peer-data-[state=unchecked]:opacity-50 transition-opacity" /> - deleteHeaders(index)} - /> + > + +
))} {/* - - {shareLink && ( -
- - -
- - copyToClipboard(shareLink)} - > - - -
-
- - Your link will expire in {selectedExpirationTime} -
-
- )} - - - ); -}; - -export default SharePopover; diff --git a/crates/web/frontend/src/components/share-popover/share-popover-utils.ts b/crates/web/frontend/src/components/share-tab/share-tab-utils.ts similarity index 100% rename from crates/web/frontend/src/components/share-popover/share-popover-utils.ts rename to crates/web/frontend/src/components/share-tab/share-tab-utils.ts diff --git a/crates/web/frontend/src/components/share-tab/share-tab.tsx b/crates/web/frontend/src/components/share-tab/share-tab.tsx new file mode 100644 index 00000000..767638a3 --- /dev/null +++ b/crates/web/frontend/src/components/share-tab/share-tab.tsx @@ -0,0 +1,172 @@ +import { cn, copyToClipboard } from "@/lib/utils"; +import type { ExpirationTime } from "@/model/expiration-time"; +import type FileType from "@/model/file-type"; +import { Clipboard, Clock, InfoIcon } from "lucide-react"; +import { type MutableRefObject, useCallback, useState } from "react"; +import ActionButton from "../action-button/action-button"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import { SidebarContent, SidebarDescription, SidebarHeader, SidebarTitle } from "../ui/sidebar"; +import { Loader } from "../ui/sonner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { createShareLink } from "./share-tab-utils"; + +interface ShareTabProps { + inputContent: MutableRefObject; + inputType: MutableRefObject; + queryContent: MutableRefObject; + outputType: MutableRefObject; + shareLink: string | undefined; + setShareLink: (shareLink?: string) => void; + className?: string; +} + +const ShareTab = ({ + inputContent, + inputType, + queryContent, + outputType, + shareLink, + setShareLink, + className, +}: ShareTabProps) => { + const [expirationTime, setExpirationTime] = useState("1 hour"); + const [selectedExpirationTime, setSelectedExpirationTime] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + const shareLink = await createShareLink( + inputContent.current, + inputType.current, + queryContent.current, + outputType.current, + expirationTime, + ); + setIsLoading(false); + if (!shareLink) return; + setShareLink(shareLink); + setSelectedExpirationTime(expirationTime); + }, + [expirationTime, setShareLink, inputContent, inputType, queryContent, outputType], + ); + + const handleChangeExpirationTime = useCallback( + (value: string) => { + setShareLink(undefined); + setExpirationTime(value as ExpirationTime); + }, + [setShareLink], + ); + + return ( + + +
+ Share your playground + + + + + +

+ When generating a sharable link,{" "} + + the content of the input json and the query will be saved in the server + {" "} + until the expiration time is reached +

+
+
+
+ + Create a shareable link to your current playground state + +
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ {shareLink && ( +
+ +
+ + copyToClipboard(shareLink)} + > + + +
+
+ + Your link will expire in {selectedExpirationTime} +
+
+ )} +
+ ); +}; + +export default ShareTab; diff --git a/crates/web/frontend/src/components/shortcut-popup/shortcut-popup.tsx b/crates/web/frontend/src/components/shortcut-popup/shortcut-popup.tsx index 49351f03..a0563100 100644 --- a/crates/web/frontend/src/components/shortcut-popup/shortcut-popup.tsx +++ b/crates/web/frontend/src/components/shortcut-popup/shortcut-popup.tsx @@ -14,17 +14,26 @@ import Link from "../ui/link"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; import { shortcutSections } from "./shortcuts"; -const ShortcutPopup = () => { +interface Props { + className?: string; +} + +const ShortcutPopup = ({ className }: Props) => { const [open, setOpen] = useState(false); return ( - - + + - + setOpen(false)} @@ -35,29 +44,34 @@ const ShortcutPopup = () => { Check all available keyboard shortcuts to improve your efficiency - {shortcutSections(isMac).map((shortcutSection) => ( -
-

{shortcutSection.title}

- - - - Description - Shortcut - - - - {shortcutSection.shortcuts.map((shortcut) => ( - - {shortcut.description} - - {shortcut.shortcut} - +
+ {shortcutSections(isMac).map((shortcutSection) => ( +
+
+ {shortcutSection.icon} +

{shortcutSection.title}

+
+
+ + + Description + Shortcut - ))} - -
-
- ))} + + + {shortcutSection.shortcuts.map((shortcut) => ( + + {shortcut.description} + + {shortcut.shortcut} + + + ))} + + +
+ ))} +
[ description: "Apply the current query", shortcut: `${isMac ? "⌘" : "Ctrl"} + Enter`, }, + { + description: "Toggle the left sidebar", + shortcut: `${isMac ? "⌘" : "Ctrl"} + B`, + }, ]; const editorShortcuts = (isMac: boolean) => [ @@ -29,10 +36,12 @@ const editorShortcuts = (isMac: boolean) => [ export const shortcutSections = (isMac: boolean): ShortcutSection[] => [ { title: "Global Scope", + icon: , shortcuts: globalShortcuts(isMac), }, { title: "Editor Scope", + icon: , shortcuts: editorShortcuts(isMac), }, ]; diff --git a/crates/web/frontend/src/components/star-count/star-count.tsx b/crates/web/frontend/src/components/star-count/star-count.tsx index 6b28d218..48127158 100644 --- a/crates/web/frontend/src/components/star-count/star-count.tsx +++ b/crates/web/frontend/src/components/star-count/star-count.tsx @@ -1,6 +1,7 @@ -import { cn, formatNumber } from "@/lib/utils"; +import { formatNumber } from "@/lib/utils"; import { Github, Star } from "lucide-react"; import { type MouseEvent, useCallback, useEffect, useState } from "react"; +import ActionButton from "../action-button/action-button"; import { Loader } from "../ui/sonner"; interface Props { @@ -12,7 +13,7 @@ const StarCount = ({ className }: Props) => { const [clicked, setClicked] = useState(false); const [fetching, setFetching] = useState(true); - const handleClick = useCallback((e: MouseEvent) => { + const handleClick = useCallback((e: MouseEvent) => { setClicked(true); setTimeout(() => { window.open("https://github.com/jorgehermo9/gq", "_blank"); @@ -30,24 +31,24 @@ const StarCount = ({ className }: Props) => { }, []); return ( - - - {fetching ? ( - - ) : ( -
- {stars} - -
- )} -
+ + + {fetching ? ( + + ) : ( +
+ {stars} + +
+ )} +
+ ); }; diff --git a/crates/web/frontend/src/components/theme-button/theme-button.tsx b/crates/web/frontend/src/components/theme-button/theme-button.tsx index 12df9b55..68c568d0 100644 --- a/crates/web/frontend/src/components/theme-button/theme-button.tsx +++ b/crates/web/frontend/src/components/theme-button/theme-button.tsx @@ -12,18 +12,27 @@ import { MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; import ActionButton from "../action-button/action-button"; -const ThemeButton = () => { +interface Props { + className?: string; +} + +const ThemeButton = ({ className }: Props) => { const { themes, theme: currentTheme, setTheme } = useTheme(); return ( - - - + + + - + Color theme diff --git a/crates/web/frontend/src/components/ui/accordion.tsx b/crates/web/frontend/src/components/ui/accordion.tsx index 0f14f76e..3c229f71 100644 --- a/crates/web/frontend/src/components/ui/accordion.tsx +++ b/crates/web/frontend/src/components/ui/accordion.tsx @@ -12,7 +12,7 @@ const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); AccordionItem.displayName = "AccordionItem"; @@ -45,7 +45,7 @@ const AccordionContent = React.forwardRef< className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" {...props} > -
{children}
+
{children}
)); diff --git a/crates/web/frontend/src/components/ui/alert-dialog.tsx b/crates/web/frontend/src/components/ui/alert-dialog.tsx index 51018be6..aee12d97 100644 --- a/crates/web/frontend/src/components/ui/alert-dialog.tsx +++ b/crates/web/frontend/src/components/ui/alert-dialog.tsx @@ -3,7 +3,7 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import * as React from "react"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button, type ButtonProps, buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; const AlertDialog = AlertDialogPrimitive.Root; @@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef< ) => ( -
+
); AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
+
); AlertDialogFooter.displayName = "AlertDialogFooter"; @@ -96,14 +96,8 @@ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -