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
-
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 (
-
);
};
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.
+
+
+ {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 && (
+ handleLoadMore()}
+ variant="outline"
+ >
+ Load more
+
+ )}
+
+ );
+};
+
+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 (