From c640acd2a9073bcc7033e58b1885ef47eeea07cc Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Tue, 22 Oct 2024 23:15:28 +0530 Subject: [PATCH 01/12] added cmd k palette --- src/App.tsx | 24 +++-- src/components/CmdKMenu.tsx | 174 ++++++++++++++++++++++++++++++++++++ src/components/Header.tsx | 2 + 3 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 src/components/CmdKMenu.tsx diff --git a/src/App.tsx b/src/App.tsx index 6f95ef41..230bafdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,20 @@ -import { CodeAndPreview } from "@/components/CodeAndPreview" -import Header from "@/components/Header" -import { useEffect } from "react" -import { useCurrentSnippetId } from "@/hooks/use-current-snippet-id" -import { useSnippet } from "./hooks/use-snippet" +import { Toaster } from "@/components/ui/toaster" +import { Route, Switch } from "wouter" +import "./components/CmdKMenu" import { ContextProviders } from "./ContextProviders" -import { EditorPage } from "./pages/editor" +import { AiPage } from "./pages/ai" +import AuthenticatePage from "./pages/authorize" import { DashboardPage } from "./pages/dashboard" -import { ViewSnippetPage } from "./pages/view-snippet" +import { EditorPage } from "./pages/editor" import { LandingPage } from "./pages/landing" -import { Route, Switch } from "wouter" -import { AiPage } from "./pages/ai" +import { MyOrdersPage } from "./pages/my-orders" import { NewestPage } from "./pages/newest" -import { SettingsPage } from "./pages/settings" -import { SearchPage } from "./pages/search" import { QuickstartPage } from "./pages/quickstart" -import { Toaster } from "@/components/ui/toaster" -import AuthenticatePage from "./pages/authorize" +import { SearchPage } from "./pages/search" +import { SettingsPage } from "./pages/settings" import { UserProfilePage } from "./pages/user-profile" -import { MyOrdersPage } from "./pages/my-orders" import { ViewOrderPage } from "./pages/view-order" +import { ViewSnippetPage } from "./pages/view-snippet" function App() { return ( diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx new file mode 100644 index 00000000..5e20eb76 --- /dev/null +++ b/src/components/CmdKMenu.tsx @@ -0,0 +1,174 @@ +import * as React from "react" +import { Link } from "wouter" +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "./ui/command" + +const CmdKMenu = () => { + const [open, setOpen] = React.useState(false) + const [searchQuery, setSearchQuery] = React.useState("") + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault() + setOpen((prev) => !prev) + } + } + window.addEventListener("keydown", handleKeyDown) + return () => { + window.removeEventListener("keydown", handleKeyDown) + } + }, []) + + // All available blank templates + const blankTemplates = [ + { name: "Blank Circuit Board", type: "board" }, + { name: "Blank Circuit Module", type: "package" }, + { name: "Blank 3D Model", type: "model", disabled: true }, + { name: "Blank Footprint", type: "footprint", disabled: true }, + ] + + // All available templates + const templates = [{ name: "Blinking LED Board", type: "board" }] + + // Import options + const importOptions = [ + { name: "KiCad Footprint", type: "footprint" }, + { name: "KiCad Project", type: "board" }, + { name: "KiCad Module", type: "package" }, + { name: "JLCPCB Component", type: "package", special: true }, + ] + + const commands = [ + { + group: "Create New", + items: blankTemplates.map((template) => ({ + label: `Create New ${template.name}`, + href: template.disabled + ? undefined + : `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, + disabled: template.disabled, + type: template.type, + })), + }, + { + group: "Templates", + items: templates.map((template) => ({ + label: `Use ${template.name} Template`, + href: `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, + type: template.type, + })), + }, + { + group: "Import", + items: importOptions.map((option) => ({ + label: `Import ${option.name}`, + action: option.special + ? () => { + setOpen(false) + // setIsJLCPCBDialogOpen(true); + } + : () => { + setOpen(false) + // toastNotImplemented(`${option.name} Import`); + }, + type: option.type, + })), + }, + ] + + const filteredCommands = commands + .map((group) => ({ + ...group, + items: group.items.filter((command) => + command.label.toLowerCase().includes(searchQuery.toLowerCase()), + ), + })) + .filter((group) => group.items.length > 0) + + const CommandItemWrapper = ({ + children, + href, + action, + onSelect, + disabled, + }: { + children: React.ReactNode + href?: string + action?: () => void + onSelect: () => void + disabled?: boolean + }) => { + if (disabled) { + return
{children}
+ } + if (href) { + return ( + + + {children} + + + ) + } + return ( +
+ {children} +
+ ) + } + + return ( + + + + {filteredCommands.length > 0 ? ( + filteredCommands.map((group, groupIndex) => ( + + {group.items.map((command, itemIndex) => ( + { + if (!command.disabled) { + setOpen(false) + } + }} + className={`flex items-center justify-between ${command.disabled ? "opacity-50" : ""}`} + disabled={command.disabled} + > + setOpen(false)} + disabled={command.disabled} + > +
+ {command.label} + + {command.type} + +
+
+
+ ))} +
+ )) + ) : ( + No commands found. + )} +
+
+ ) +} + +export default CmdKMenu diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d810a78b..e50e2778 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,6 +6,7 @@ import { GitHubLogoIcon } from "@radix-ui/react-icons" import { Menu, X } from "lucide-react" import React, { useState } from "react" import { Link, useLocation } from "wouter" +import CmdKMenu from "./CmdKMenu" import HeaderDropdown from "./HeaderDropdown" import SearchComponent from "./SearchComponent" @@ -155,6 +156,7 @@ export default function Header() { )} + ) } From 63a8d5ae2812f4476667cb722fd264225d0c4efb Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Tue, 22 Oct 2024 23:34:07 +0530 Subject: [PATCH 02/12] added commands with reference to quickstart page --- src/components/CmdKMenu.tsx | 213 +++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 89 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 5e20eb76..1989eb59 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -1,5 +1,4 @@ -import * as React from "react" -import { Link } from "wouter" +import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog" import { CommandDialog, CommandEmpty, @@ -7,11 +6,49 @@ import { CommandInput, CommandItem, CommandList, -} from "./ui/command" +} from "@/components/ui/command" +import { useAxios } from "@/hooks/use-axios" +import { useGlobalStore } from "@/hooks/use-global-store" +import { useNotImplementedToast } from "@/hooks/use-toast" +import { Snippet } from "fake-snippets-api/lib/db/schema" +import React from "react" +import { useQuery } from "react-query" -const CmdKMenu = () => { +type SnippetType = "board" | "package" | "model" | "footprint" | "snippet" + +interface Template { + name: string + type: SnippetType + disabled?: boolean +} + +interface ImportOption { + name: string + type: SnippetType + special?: boolean +} + +interface CommandItemData { + label: string + href?: string + type: SnippetType + disabled?: boolean + action?: () => void + subtitle?: string +} + +interface CommandGroup { + group: string + items: CommandItemData[] +} + +const CmdKMenu: React.FC = () => { const [open, setOpen] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState("") + const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false) + const toastNotImplemented = useNotImplementedToast() + const axios = useAxios() + const currentUser = useGlobalStore((s) => s.session?.github_username) React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -20,14 +57,27 @@ const CmdKMenu = () => { setOpen((prev) => !prev) } } + window.addEventListener("keydown", handleKeyDown) - return () => { - window.removeEventListener("keydown", handleKeyDown) - } + return () => window.removeEventListener("keydown", handleKeyDown) }, []) + const { data: recentSnippets } = useQuery( + ["userSnippets", currentUser], + async () => { + if (!currentUser) return [] + const response = await axios.get<{ snippets: Snippet[] }>( + `/snippets/list?owner_name=${currentUser}`, + ) + return response.data.snippets + }, + { + enabled: !!currentUser, + }, + ) + // All available blank templates - const blankTemplates = [ + const blankTemplates: Template[] = [ { name: "Blank Circuit Board", type: "board" }, { name: "Blank Circuit Module", type: "package" }, { name: "Blank 3D Model", type: "model", disabled: true }, @@ -35,32 +85,41 @@ const CmdKMenu = () => { ] // All available templates - const templates = [{ name: "Blinking LED Board", type: "board" }] + const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }] // Import options - const importOptions = [ + const importOptions: ImportOption[] = [ { name: "KiCad Footprint", type: "footprint" }, { name: "KiCad Project", type: "board" }, { name: "KiCad Module", type: "package" }, { name: "JLCPCB Component", type: "package", special: true }, ] - const commands = [ + const commands: CommandGroup[] = [ { - group: "Create New", + group: "Recent Snippets", + items: (recentSnippets?.slice(0, 6) || []).map((snippet) => ({ + label: snippet.unscoped_name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "snippet" as const, + subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, + })), + }, + { + group: "Start Blank Snippet", items: blankTemplates.map((template) => ({ - label: `Create New ${template.name}`, + label: template.name, href: template.disabled ? undefined : `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, - disabled: template.disabled, type: template.type, + disabled: template.disabled, })), }, { - group: "Templates", + group: "Start from Template", items: templates.map((template) => ({ - label: `Use ${template.name} Template`, + label: template.name, href: `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, type: template.type, })), @@ -72,11 +131,11 @@ const CmdKMenu = () => { action: option.special ? () => { setOpen(false) - // setIsJLCPCBDialogOpen(true); + setIsJLCPCBDialogOpen(true) } : () => { setOpen(false) - // toastNotImplemented(`${option.name} Import`); + toastNotImplemented(`${option.name} Import`) }, type: option.type, })), @@ -92,82 +151,58 @@ const CmdKMenu = () => { })) .filter((group) => group.items.length > 0) - const CommandItemWrapper = ({ - children, - href, - action, - onSelect, - disabled, - }: { - children: React.ReactNode - href?: string - action?: () => void - onSelect: () => void - disabled?: boolean - }) => { - if (disabled) { - return
{children}
- } - if (href) { - return ( - - - {children} - - - ) - } - return ( -
- {children} -
- ) - } - return ( - - - - {filteredCommands.length > 0 ? ( - filteredCommands.map((group, groupIndex) => ( - - {group.items.map((command, itemIndex) => ( - { - if (!command.disabled) { - setOpen(false) - } - }} - className={`flex items-center justify-between ${command.disabled ? "opacity-50" : ""}`} - disabled={command.disabled} - > - setOpen(false)} + <> + + + + {filteredCommands.length > 0 ? ( + filteredCommands.map((group, groupIndex) => ( + + {group.items.map((command, itemIndex) => ( + { + if (command.action) { + command.action() + } else if (command.href && !command.disabled) { + window.location.href = command.href + setOpen(false) + } + }} disabled={command.disabled} + className="flex items-center justify-between" > -
+
{command.label} - - {command.type} - + {command.subtitle && ( + + {command.subtitle} + + )}
- - - ))} - - )) - ) : ( - No commands found. - )} - - + + {command.type} + + + ))} + + )) + ) : ( + No commands found. + )} + + + + + ) } From 71a705426eb2e974555222a852653c4bdfde473b Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Tue, 22 Oct 2024 23:48:10 +0530 Subject: [PATCH 03/12] added search functionality to cmd k bar --- src/components/CmdKMenu.tsx | 56 +++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 1989eb59..9f91137f 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -14,7 +14,13 @@ import { Snippet } from "fake-snippets-api/lib/db/schema" import React from "react" import { useQuery } from "react-query" -type SnippetType = "board" | "package" | "model" | "footprint" | "snippet" +type SnippetType = + | "board" + | "package" + | "model" + | "footprint" + | "snippet" + | "search-result" interface Template { name: string @@ -35,6 +41,7 @@ interface CommandItemData { disabled?: boolean action?: () => void subtitle?: string + description?: string } interface CommandGroup { @@ -76,7 +83,22 @@ const CmdKMenu: React.FC = () => { }, ) - // All available blank templates + // Add search results query + const { data: searchResults, isLoading: isSearching } = useQuery( + ["snippetSearch", searchQuery], + async () => { + if (!searchQuery) return [] + const { data } = await axios.get("/snippets/search", { + params: { q: searchQuery }, + }) + return data.snippets + }, + { + enabled: Boolean(searchQuery), + }, + ) + + // Templates and import options const blankTemplates: Template[] = [ { name: "Blank Circuit Board", type: "board" }, { name: "Blank Circuit Module", type: "package" }, @@ -84,10 +106,8 @@ const CmdKMenu: React.FC = () => { { name: "Blank Footprint", type: "footprint", disabled: true }, ] - // All available templates const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }] - // Import options const importOptions: ImportOption[] = [ { name: "KiCad Footprint", type: "footprint" }, { name: "KiCad Project", type: "board" }, @@ -95,7 +115,22 @@ const CmdKMenu: React.FC = () => { { name: "JLCPCB Component", type: "package", special: true }, ] + // Combine regular commands with search results const commands: CommandGroup[] = [ + ...(searchResults && searchResults.length > 0 + ? [ + { + group: "Search Results", + items: searchResults.map((snippet: any) => ({ + label: snippet.name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "search-result" as const, + subtitle: `By ${snippet.owner_name}`, + description: snippet.description, + })), + }, + ] + : []), { group: "Recent Snippets", items: (recentSnippets?.slice(0, 6) || []).map((snippet) => ({ @@ -160,6 +195,12 @@ const CmdKMenu: React.FC = () => { onValueChange={setSearchQuery} /> + {isSearching && ( + + Searching... + + )} + {filteredCommands.length > 0 ? ( filteredCommands.map((group, groupIndex) => ( @@ -184,6 +225,11 @@ const CmdKMenu: React.FC = () => { {command.subtitle} )} + {command.description && ( + + {command.description} + + )}
{command.type} @@ -193,7 +239,7 @@ const CmdKMenu: React.FC = () => {
)) ) : ( - No commands found. + No results found. )}
From 3943878bd62a2169d3ca6e0872992672846f5ca2 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 08:18:52 +0530 Subject: [PATCH 04/12] fixed search filtering in cmdK --- src/components/CmdKMenu.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 9f91137f..eee12406 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -180,9 +180,12 @@ const CmdKMenu: React.FC = () => { const filteredCommands = commands .map((group) => ({ ...group, - items: group.items.filter((command) => - command.label.toLowerCase().includes(searchQuery.toLowerCase()), - ), + items: + group.group === "Search Results" + ? group.items + : group.items.filter((command) => + command.label.toLowerCase().includes(searchQuery.toLowerCase()), + ), })) .filter((group) => group.items.length > 0) From 56d3468390c6ada57f73d2cd47c652377e0770c7 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 08:55:20 +0530 Subject: [PATCH 05/12] better search filtering --- src/components/CmdKMenu.tsx | 101 +++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 66a4a011..ce54af6a 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -1,4 +1,4 @@ -import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog" +import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog"; import { CommandDialog, CommandEmpty, @@ -6,13 +6,13 @@ import { CommandInput, CommandItem, CommandList, -} from "@/components/ui/command" -import { useAxios } from "@/hooks/use-axios" -import { useGlobalStore } from "@/hooks/use-global-store" -import { useNotImplementedToast } from "@/hooks/use-toast" -import { Snippet } from "fake-snippets-api/lib/db/schema" -import React from "react" -import { useQuery } from "react-query" +} from "@/components/ui/command"; +import { useAxios } from "@/hooks/use-axios"; +import { useGlobalStore } from "@/hooks/use-global-store"; +import { useNotImplementedToast } from "@/hooks/use-toast"; +import { Snippet } from "fake-snippets-api/lib/db/schema"; +import React from 'react'; +import { useQuery } from "react-query"; type SnippetType = | "board" @@ -83,7 +83,6 @@ const CmdKMenu: React.FC = () => { }, ) - // Add search results query const { data: searchResults, isLoading: isSearching } = useQuery( ["snippetSearch", searchQuery], async () => { @@ -115,23 +114,10 @@ const CmdKMenu: React.FC = () => { { name: "JLCPCB Component", type: "package", special: true }, ] - // Combine regular commands with search results - const commands: CommandGroup[] = [ - ...(searchResults && searchResults.length > 0 - ? [ - { - group: "Search Results", - items: searchResults.map((snippet: any) => ({ - label: snippet.name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "search-result" as const, - subtitle: `By ${snippet.owner_name}`, - description: snippet.description, - })), - }, - ] - : []), - { + // Build the base command groups without search results + const baseCommands: CommandGroup[] = [ + // Only include Recent Snippets when there's no search query + ...(!searchQuery ? [{ group: "Recent Snippets", items: (recentSnippets?.slice(0, 6) || []).map((snippet) => ({ label: snippet.unscoped_name, @@ -139,7 +125,7 @@ const CmdKMenu: React.FC = () => { type: "snippet" as const, subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, })), - }, + }] : []), { group: "Start Blank Snippet", items: blankTemplates.map((template) => ({ @@ -177,18 +163,43 @@ const CmdKMenu: React.FC = () => { }, ] - const filteredCommands = commands - .map((group) => ({ - ...group, - items: - group.group === "Search Results" - ? group.items - : group.items.filter((command) => - command.label.toLowerCase().includes(searchQuery.toLowerCase()), - ), + // Filter base commands based on search query + const filteredBaseCommands = searchQuery + ? baseCommands + .map((group) => ({ + ...group, + items: group.items.filter((command) => + command.label.toLowerCase().includes(searchQuery.toLowerCase()), + ), + })) + .filter((group) => group.items.length > 0) + : baseCommands - })) - .filter((group) => group.items.length > 0) + // Combine search results with filtered base commands + const allCommands = [ + ...(isSearching + ? [ + { + group: "Search Results", + items: [{ label: "Searching...", type: "search-result" as const, disabled: true }], + }, + ] + : searchResults && searchResults.length > 0 + ? [ + { + group: "Search Results", + items: searchResults.map((snippet: any) => ({ + label: snippet.name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "search-result" as const, + subtitle: `By ${snippet.owner_name}`, + description: snippet.description, + })), + }, + ] + : []), + ...filteredBaseCommands, + ] return ( <> @@ -199,16 +210,10 @@ const CmdKMenu: React.FC = () => { onValueChange={setSearchQuery} /> - {isSearching && ( - - Searching... - - )} - - {filteredCommands.length > 0 ? ( - filteredCommands.map((group, groupIndex) => ( + {allCommands.length > 0 ? ( + allCommands.map((group, groupIndex) => ( - {group.items.map((command, itemIndex) => ( + {group.items.map((command: CommandItemData, itemIndex: number) => ( { @@ -256,4 +261,4 @@ const CmdKMenu: React.FC = () => { ) } -export default CmdKMenu +export default CmdKMenu \ No newline at end of file From 721872e549703aa6c0c521628b124f590aa56019 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 09:00:55 +0530 Subject: [PATCH 06/12] added debouncing --- src/components/CmdKMenu.tsx | 45 ++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index ce54af6a..f8bde973 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -49,9 +49,27 @@ interface CommandGroup { items: CommandItemData[] } +// Debounce helper function +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = React.useState(value); + + React.useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} + const CmdKMenu: React.FC = () => { const [open, setOpen] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState("") + const debouncedSearchQuery = useDebounce(searchQuery, 300) // 300ms debounce const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false) const toastNotImplemented = useNotImplementedToast() const axios = useAxios() @@ -84,16 +102,19 @@ const CmdKMenu: React.FC = () => { ) const { data: searchResults, isLoading: isSearching } = useQuery( - ["snippetSearch", searchQuery], + ["snippetSearch", debouncedSearchQuery], // Use debounced query async () => { - if (!searchQuery) return [] + if (!debouncedSearchQuery) return [] const { data } = await axios.get("/snippets/search", { - params: { q: searchQuery }, + params: { q: debouncedSearchQuery }, }) return data.snippets }, { - enabled: Boolean(searchQuery), + enabled: Boolean(debouncedSearchQuery), + keepPreviousData: true, + staleTime: 30000, // Cache results for 30 seconds + retry: false, }, ) @@ -114,10 +135,9 @@ const CmdKMenu: React.FC = () => { { name: "JLCPCB Component", type: "package", special: true }, ] - // Build the base command groups without search results const baseCommands: CommandGroup[] = [ // Only include Recent Snippets when there's no search query - ...(!searchQuery ? [{ + ...(!debouncedSearchQuery ? [{ group: "Recent Snippets", items: (recentSnippets?.slice(0, 6) || []).map((snippet) => ({ label: snippet.unscoped_name, @@ -163,13 +183,12 @@ const CmdKMenu: React.FC = () => { }, ] - // Filter base commands based on search query - const filteredBaseCommands = searchQuery + const filteredBaseCommands = debouncedSearchQuery ? baseCommands .map((group) => ({ ...group, items: group.items.filter((command) => - command.label.toLowerCase().includes(searchQuery.toLowerCase()), + command.label.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), ), })) .filter((group) => group.items.length > 0) @@ -181,7 +200,13 @@ const CmdKMenu: React.FC = () => { ? [ { group: "Search Results", - items: [{ label: "Searching...", type: "search-result" as const, disabled: true }], + items: searchResult.length ? searchResults.map((snippet: any) => ({ + label: snippet.name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "search-result" as const, + subtitle: `By ${snippet.owner_name}`, + description: snippet.description, + })) : [{ label: "Searching...", type: "search-result" as const, disabled: true }], }, ] : searchResults && searchResults.length > 0 From d4ca30241964706609ac1adf1063f6cb177366a4 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 19:33:35 +0530 Subject: [PATCH 07/12] rewrote cmd k logic for consistency --- src/components/CmdKMenu.tsx | 338 +++++++++++++++++------------------- 1 file changed, 162 insertions(+), 176 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index f8bde973..01566071 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -14,217 +14,199 @@ import { Snippet } from "fake-snippets-api/lib/db/schema"; import React from 'react'; import { useQuery } from "react-query"; -type SnippetType = - | "board" - | "package" - | "model" - | "footprint" - | "snippet" - | "search-result" +type SnippetType = "board" | "package" | "model" | "footprint" | "snippet"; interface Template { - name: string - type: SnippetType - disabled?: boolean + name: string; + type: SnippetType; + disabled?: boolean; } interface ImportOption { - name: string - type: SnippetType - special?: boolean + name: string; + type: SnippetType; + special?: boolean; } interface CommandItemData { - label: string - href?: string - type: SnippetType - disabled?: boolean - action?: () => void - subtitle?: string - description?: string + label: string; + href?: string; + type: SnippetType; + disabled?: boolean; + action?: () => void; + subtitle?: string; + description?: string; } interface CommandGroup { - group: string - items: CommandItemData[] + group: string; + items: CommandItemData[]; } -// Debounce helper function -function useDebounce(value: T, delay: number): T { - const [debouncedValue, setDebouncedValue] = React.useState(value); - - React.useEffect(() => { - const timer = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - return () => { - clearTimeout(timer); - }; - }, [value, delay]); - - return debouncedValue; +function fuzzySearch(str: string, query: string): boolean { + const pattern = query.split('').map(char => + char.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ).join('.*'); + const regex = new RegExp(pattern, 'i'); + return regex.test(str); } const CmdKMenu: React.FC = () => { - const [open, setOpen] = React.useState(false) - const [searchQuery, setSearchQuery] = React.useState("") - const debouncedSearchQuery = useDebounce(searchQuery, 300) // 300ms debounce - const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false) - const toastNotImplemented = useNotImplementedToast() - const axios = useAxios() - const currentUser = useGlobalStore((s) => s.session?.github_username) + const [open, setOpen] = React.useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false); + const toastNotImplemented = useNotImplementedToast(); + const axios = useAxios(); + const currentUser = useGlobalStore((s) => s.session?.github_username); - React.useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault() - setOpen((prev) => !prev) - } + // Search results query + const { data: searchResults, isLoading: isSearching } = useQuery( + ["snippetSearch", searchQuery], + async () => { + if (!searchQuery) return []; + const { data } = await axios.get("/snippets/search", { + params: { q: searchQuery }, + }); + return data.snippets; + }, + { + enabled: Boolean(searchQuery), } + ); - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, []) - + // Recent snippets query (only when not searching) const { data: recentSnippets } = useQuery( ["userSnippets", currentUser], async () => { - if (!currentUser) return [] + if (!currentUser) return []; const response = await axios.get<{ snippets: Snippet[] }>( `/snippets/list?owner_name=${currentUser}`, - ) - return response.data.snippets + ); + return response.data.snippets; }, { - enabled: !!currentUser, - }, - ) + enabled: !!currentUser && !searchQuery, + } + ); - const { data: searchResults, isLoading: isSearching } = useQuery( - ["snippetSearch", debouncedSearchQuery], // Use debounced query - async () => { - if (!debouncedSearchQuery) return [] - const { data } = await axios.get("/snippets/search", { - params: { q: debouncedSearchQuery }, - }) - return data.snippets - }, - { - enabled: Boolean(debouncedSearchQuery), - keepPreviousData: true, - staleTime: 30000, // Cache results for 30 seconds - retry: false, - }, - ) + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setOpen((prev) => !prev); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); - // Templates and import options const blankTemplates: Template[] = [ { name: "Blank Circuit Board", type: "board" }, { name: "Blank Circuit Module", type: "package" }, { name: "Blank 3D Model", type: "model", disabled: true }, { name: "Blank Footprint", type: "footprint", disabled: true }, - ] + ]; - const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }] + const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }]; const importOptions: ImportOption[] = [ { name: "KiCad Footprint", type: "footprint" }, { name: "KiCad Project", type: "board" }, { name: "KiCad Module", type: "package" }, { name: "JLCPCB Component", type: "package", special: true }, - ] + ]; - const baseCommands: CommandGroup[] = [ - // Only include Recent Snippets when there's no search query - ...(!debouncedSearchQuery ? [{ - group: "Recent Snippets", - items: (recentSnippets?.slice(0, 6) || []).map((snippet) => ({ - label: snippet.unscoped_name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "snippet" as const, - subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, - })), - }] : []), - { - group: "Start Blank Snippet", - items: blankTemplates.map((template) => ({ - label: template.name, - href: template.disabled - ? undefined - : `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, - type: template.type, - disabled: template.disabled, - })), - }, - { - group: "Start from Template", - items: templates.map((template) => ({ - label: template.name, - href: `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, - type: template.type, - })), - }, - { - group: "Import", - items: importOptions.map((option) => ({ - label: `Import ${option.name}`, - action: option.special - ? () => { - setOpen(false) - setIsJLCPCBDialogOpen(true) - } - : () => { - setOpen(false) - toastNotImplemented(`${option.name} Import`) - }, - type: option.type, - })), - }, - ] + const baseCommands = React.useMemo(() => { + const commands: CommandGroup[] = []; + + // Only show search results if there's a search query + if (searchQuery && searchResults?.length) { + commands.push({ + group: "Search Results", + items: searchResults.map((snippet: Snippet) => ({ + label: snippet.name || snippet.unscoped_name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "snippet" as const, + description: snippet.description, + subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, + })), + }); + } + + // Only show recent snippets if there's no search query + if (!searchQuery && recentSnippets?.length) { + commands.push({ + group: "Recent Snippets", + items: recentSnippets.slice(0, 6).map((snippet) => ({ + label: snippet.unscoped_name, + href: `/editor?snippet_id=${snippet.snippet_id}`, + type: "snippet" as const, + subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, + })), + }); + } + + commands.push( + { + group: "Start Blank Snippet", + items: blankTemplates.map((template) => ({ + label: template.name, + href: template.disabled + ? undefined + : `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, + type: template.type, + disabled: template.disabled, + })), + }, + { + group: "Start from Template", + items: templates.map((template) => ({ + label: template.name, + href: `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, + type: template.type, + })), + }, + { + group: "Import", + items: importOptions.map((option) => ({ + label: `Import ${option.name}`, + action: option.special + ? () => { + setOpen(false); + setIsJLCPCBDialogOpen(true); + } + : () => { + setOpen(false); + toastNotImplemented(`${option.name} Import`); + }, + type: option.type, + })), + } + ); - const filteredBaseCommands = debouncedSearchQuery - ? baseCommands - .map((group) => ({ - ...group, - items: group.items.filter((command) => - command.label.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), - ), - })) - .filter((group) => group.items.length > 0) - : baseCommands + return commands; + }, [searchQuery, searchResults, recentSnippets, blankTemplates, templates, importOptions]); - // Combine search results with filtered base commands - const allCommands = [ - ...(isSearching - ? [ - { - group: "Search Results", - items: searchResult.length ? searchResults.map((snippet: any) => ({ - label: snippet.name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "search-result" as const, - subtitle: `By ${snippet.owner_name}`, - description: snippet.description, - })) : [{ label: "Searching...", type: "search-result" as const, disabled: true }], - }, - ] - : searchResults && searchResults.length > 0 - ? [ - { - group: "Search Results", - items: searchResults.map((snippet: any) => ({ - label: snippet.name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "search-result" as const, - subtitle: `By ${snippet.owner_name}`, - description: snippet.description, - })), - }, - ] - : []), - ...filteredBaseCommands, - ] + const filteredCommands = React.useMemo(() => { + if (!searchQuery) return baseCommands; + + return baseCommands + .map(group => ({ + ...group, + items: group.items.filter(command => { + const searchString = [ + command.label, + command.subtitle, + command.description, + command.type + ].filter(Boolean).join(' ').toLowerCase(); + + return fuzzySearch(searchString, searchQuery.toLowerCase()); + }) + })) + .filter(group => group.items.length > 0); + }, [baseCommands, searchQuery]); return ( <> @@ -235,18 +217,22 @@ const CmdKMenu: React.FC = () => { onValueChange={setSearchQuery} /> - {allCommands.length > 0 ? ( - allCommands.map((group, groupIndex) => ( + {isSearching ? ( + + Loading results... + + ) : filteredCommands.length > 0 ? ( + filteredCommands.map((group, groupIndex) => ( {group.items.map((command: CommandItemData, itemIndex: number) => ( { if (command.action) { - command.action() + command.action(); } else if (command.href && !command.disabled) { - window.location.href = command.href - setOpen(false) + window.location.href = command.href; + setOpen(false); } }} disabled={command.disabled} @@ -283,7 +269,7 @@ const CmdKMenu: React.FC = () => { onOpenChange={setIsJLCPCBDialogOpen} /> - ) -} + ); +}; -export default CmdKMenu \ No newline at end of file +export default CmdKMenu; \ No newline at end of file From 9d4dd628d57f7b92d150de99392997d8cb3b77b9 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 19:37:38 +0530 Subject: [PATCH 08/12] format --- src/components/CmdKMenu.tsx | 235 +++++++++++++++++++----------------- 1 file changed, 124 insertions(+), 111 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 01566071..c92410d1 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -1,4 +1,4 @@ -import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog"; +import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog" import { CommandDialog, CommandEmpty, @@ -6,119 +6,120 @@ import { CommandInput, CommandItem, CommandList, -} from "@/components/ui/command"; -import { useAxios } from "@/hooks/use-axios"; -import { useGlobalStore } from "@/hooks/use-global-store"; -import { useNotImplementedToast } from "@/hooks/use-toast"; -import { Snippet } from "fake-snippets-api/lib/db/schema"; -import React from 'react'; -import { useQuery } from "react-query"; +} from "@/components/ui/command" +import { useAxios } from "@/hooks/use-axios" +import { useGlobalStore } from "@/hooks/use-global-store" +import { useNotImplementedToast } from "@/hooks/use-toast" +import { Snippet } from "fake-snippets-api/lib/db/schema" +import React from "react" +import { useQuery } from "react-query" -type SnippetType = "board" | "package" | "model" | "footprint" | "snippet"; +type SnippetType = "board" | "package" | "model" | "footprint" | "snippet" interface Template { - name: string; - type: SnippetType; - disabled?: boolean; + name: string + type: SnippetType + disabled?: boolean } interface ImportOption { - name: string; - type: SnippetType; - special?: boolean; + name: string + type: SnippetType + special?: boolean } interface CommandItemData { - label: string; - href?: string; - type: SnippetType; - disabled?: boolean; - action?: () => void; - subtitle?: string; - description?: string; + label: string + href?: string + type: SnippetType + disabled?: boolean + action?: () => void + subtitle?: string + description?: string } interface CommandGroup { - group: string; - items: CommandItemData[]; + group: string + items: CommandItemData[] } function fuzzySearch(str: string, query: string): boolean { - const pattern = query.split('').map(char => - char.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') - ).join('.*'); - const regex = new RegExp(pattern, 'i'); - return regex.test(str); + const pattern = query + .split("") + .map((char) => char.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) + .join(".*") + const regex = new RegExp(pattern, "i") + return regex.test(str) } const CmdKMenu: React.FC = () => { - const [open, setOpen] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(""); - const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false); - const toastNotImplemented = useNotImplementedToast(); - const axios = useAxios(); - const currentUser = useGlobalStore((s) => s.session?.github_username); + const [open, setOpen] = React.useState(false) + const [searchQuery, setSearchQuery] = React.useState("") + const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false) + const toastNotImplemented = useNotImplementedToast() + const axios = useAxios() + const currentUser = useGlobalStore((s) => s.session?.github_username) // Search results query const { data: searchResults, isLoading: isSearching } = useQuery( ["snippetSearch", searchQuery], async () => { - if (!searchQuery) return []; + if (!searchQuery) return [] const { data } = await axios.get("/snippets/search", { params: { q: searchQuery }, - }); - return data.snippets; + }) + return data.snippets }, { enabled: Boolean(searchQuery), - } - ); + }, + ) // Recent snippets query (only when not searching) const { data: recentSnippets } = useQuery( ["userSnippets", currentUser], async () => { - if (!currentUser) return []; + if (!currentUser) return [] const response = await axios.get<{ snippets: Snippet[] }>( `/snippets/list?owner_name=${currentUser}`, - ); - return response.data.snippets; + ) + return response.data.snippets }, { enabled: !!currentUser && !searchQuery, - } - ); + }, + ) React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault(); - setOpen((prev) => !prev); + e.preventDefault() + setOpen((prev) => !prev) } - }; + } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, []); + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, []) const blankTemplates: Template[] = [ { name: "Blank Circuit Board", type: "board" }, { name: "Blank Circuit Module", type: "package" }, { name: "Blank 3D Model", type: "model", disabled: true }, { name: "Blank Footprint", type: "footprint", disabled: true }, - ]; + ] - const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }]; + const templates: Template[] = [{ name: "Blinking LED Board", type: "board" }] const importOptions: ImportOption[] = [ { name: "KiCad Footprint", type: "footprint" }, { name: "KiCad Project", type: "board" }, { name: "KiCad Module", type: "package" }, { name: "JLCPCB Component", type: "package", special: true }, - ]; + ] const baseCommands = React.useMemo(() => { - const commands: CommandGroup[] = []; + const commands: CommandGroup[] = [] // Only show search results if there's a search query if (searchQuery && searchResults?.length) { @@ -131,9 +132,9 @@ const CmdKMenu: React.FC = () => { description: snippet.description, subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, })), - }); + }) } - + // Only show recent snippets if there's no search query if (!searchQuery && recentSnippets?.length) { commands.push({ @@ -144,7 +145,7 @@ const CmdKMenu: React.FC = () => { type: "snippet" as const, subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, })), - }); + }) } commands.push( @@ -173,40 +174,50 @@ const CmdKMenu: React.FC = () => { label: `Import ${option.name}`, action: option.special ? () => { - setOpen(false); - setIsJLCPCBDialogOpen(true); + setOpen(false) + setIsJLCPCBDialogOpen(true) } : () => { - setOpen(false); - toastNotImplemented(`${option.name} Import`); + setOpen(false) + toastNotImplemented(`${option.name} Import`) }, type: option.type, })), - } - ); + }, + ) - return commands; - }, [searchQuery, searchResults, recentSnippets, blankTemplates, templates, importOptions]); + return commands + }, [ + searchQuery, + searchResults, + recentSnippets, + blankTemplates, + templates, + importOptions, + ]) const filteredCommands = React.useMemo(() => { - if (!searchQuery) return baseCommands; + if (!searchQuery) return baseCommands return baseCommands - .map(group => ({ + .map((group) => ({ ...group, - items: group.items.filter(command => { + items: group.items.filter((command) => { const searchString = [ command.label, command.subtitle, command.description, - command.type - ].filter(Boolean).join(' ').toLowerCase(); - - return fuzzySearch(searchString, searchQuery.toLowerCase()); - }) + command.type, + ] + .filter(Boolean) + .join(" ") + .toLowerCase() + + return fuzzySearch(searchString, searchQuery.toLowerCase()) + }), })) - .filter(group => group.items.length > 0); - }, [baseCommands, searchQuery]); + .filter((group) => group.items.length > 0) + }, [baseCommands, searchQuery]) return ( <> @@ -224,38 +235,40 @@ const CmdKMenu: React.FC = () => { ) : filteredCommands.length > 0 ? ( filteredCommands.map((group, groupIndex) => ( - {group.items.map((command: CommandItemData, itemIndex: number) => ( - { - if (command.action) { - command.action(); - } else if (command.href && !command.disabled) { - window.location.href = command.href; - setOpen(false); - } - }} - disabled={command.disabled} - className="flex items-center justify-between" - > -
- {command.label} - {command.subtitle && ( - - {command.subtitle} - - )} - {command.description && ( - - {command.description} - - )} -
- - {command.type} - -
- ))} + {group.items.map( + (command: CommandItemData, itemIndex: number) => ( + { + if (command.action) { + command.action() + } else if (command.href && !command.disabled) { + window.location.href = command.href + setOpen(false) + } + }} + disabled={command.disabled} + className="flex items-center justify-between" + > +
+ {command.label} + {command.subtitle && ( + + {command.subtitle} + + )} + {command.description && ( + + {command.description} + + )} +
+ + {command.type} + +
+ ), + )}
)) ) : ( @@ -269,7 +282,7 @@ const CmdKMenu: React.FC = () => { onOpenChange={setIsJLCPCBDialogOpen} /> - ); -}; + ) +} -export default CmdKMenu; \ No newline at end of file +export default CmdKMenu From f24ace27f08cf87a290c778ac63629d3c5137f0d Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 23 Oct 2024 22:13:35 +0530 Subject: [PATCH 09/12] better way to filter search results --- src/components/CmdKMenu.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index c92410d1..6c767a96 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -198,9 +198,13 @@ const CmdKMenu: React.FC = () => { const filteredCommands = React.useMemo(() => { if (!searchQuery) return baseCommands - - return baseCommands - .map((group) => ({ + + return baseCommands.map((group) => { + if (group.group === "Search Results") { + return group + } + + return { ...group, items: group.items.filter((command) => { const searchString = [ @@ -212,13 +216,14 @@ const CmdKMenu: React.FC = () => { .filter(Boolean) .join(" ") .toLowerCase() - + return fuzzySearch(searchString, searchQuery.toLowerCase()) }), - })) - .filter((group) => group.items.length > 0) + } + }).filter((group) => + group.items.length > 0 || group.group === "Search Results" + ) }, [baseCommands, searchQuery]) - return ( <> From 1583158d47a5b08055e709750b314ae93628c910 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Thu, 24 Oct 2024 20:23:42 +0530 Subject: [PATCH 10/12] fixes searching bug, now uses CMD-K library directly --- src/components/CmdKMenu.tsx | 361 ++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 196 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 6c767a96..9ef0f216 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -1,15 +1,8 @@ import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog" -import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command" import { useAxios } from "@/hooks/use-axios" import { useGlobalStore } from "@/hooks/use-global-store" import { useNotImplementedToast } from "@/hooks/use-toast" +import { Command } from 'cmdk' import { Snippet } from "fake-snippets-api/lib/db/schema" import React from "react" import { useQuery } from "react-query" @@ -28,31 +21,7 @@ interface ImportOption { special?: boolean } -interface CommandItemData { - label: string - href?: string - type: SnippetType - disabled?: boolean - action?: () => void - subtitle?: string - description?: string -} - -interface CommandGroup { - group: string - items: CommandItemData[] -} - -function fuzzySearch(str: string, query: string): boolean { - const pattern = query - .split("") - .map((char) => char.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) - .join(".*") - const regex = new RegExp(pattern, "i") - return regex.test(str) -} - -const CmdKMenu: React.FC = () => { +const CmdKMenu = () => { const [open, setOpen] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState("") const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false) @@ -61,45 +30,45 @@ const CmdKMenu: React.FC = () => { const currentUser = useGlobalStore((s) => s.session?.github_username) // Search results query - const { data: searchResults, isLoading: isSearching } = useQuery( + const { data: searchResults = [], isLoading: isSearching } = useQuery( ["snippetSearch", searchQuery], async () => { if (!searchQuery) return [] const { data } = await axios.get("/snippets/search", { params: { q: searchQuery }, }) - return data.snippets + return data.snippets || [] }, { enabled: Boolean(searchQuery), - }, + } ) - // Recent snippets query (only when not searching) - const { data: recentSnippets } = useQuery( + // Recent snippets query + const { data: recentSnippets = [] } = useQuery( ["userSnippets", currentUser], async () => { if (!currentUser) return [] const response = await axios.get<{ snippets: Snippet[] }>( - `/snippets/list?owner_name=${currentUser}`, + `/snippets/list?owner_name=${currentUser}` ) - return response.data.snippets + return response.data.snippets || [] }, { enabled: !!currentUser && !searchQuery, - }, + } ) React.useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { + const down = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault() setOpen((prev) => !prev) } } - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) + document.addEventListener("keydown", down) + return () => document.removeEventListener("keydown", down) }, []) const blankTemplates: Template[] = [ @@ -118,176 +87,176 @@ const CmdKMenu: React.FC = () => { { name: "JLCPCB Component", type: "package", special: true }, ] - const baseCommands = React.useMemo(() => { - const commands: CommandGroup[] = [] - - // Only show search results if there's a search query - if (searchQuery && searchResults?.length) { - commands.push({ - group: "Search Results", - items: searchResults.map((snippet: Snippet) => ({ - label: snippet.name || snippet.unscoped_name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "snippet" as const, - description: snippet.description, - subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, - })), - }) - } - - // Only show recent snippets if there's no search query - if (!searchQuery && recentSnippets?.length) { - commands.push({ - group: "Recent Snippets", - items: recentSnippets.slice(0, 6).map((snippet) => ({ - label: snippet.unscoped_name, - href: `/editor?snippet_id=${snippet.snippet_id}`, - type: "snippet" as const, - subtitle: `Last edited: ${new Date(snippet.updated_at).toLocaleDateString()}`, - })), - }) - } - - commands.push( - { - group: "Start Blank Snippet", - items: blankTemplates.map((template) => ({ - label: template.name, - href: template.disabled - ? undefined - : `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, - type: template.type, - disabled: template.disabled, - })), - }, - { - group: "Start from Template", - items: templates.map((template) => ({ - label: template.name, - href: `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`, - type: template.type, - })), - }, - { - group: "Import", - items: importOptions.map((option) => ({ - label: `Import ${option.name}`, - action: option.special - ? () => { - setOpen(false) - setIsJLCPCBDialogOpen(true) - } - : () => { - setOpen(false) - toastNotImplemented(`${option.name} Import`) - }, - type: option.type, - })), - }, - ) - - return commands - }, [ - searchQuery, - searchResults, - recentSnippets, - blankTemplates, - templates, - importOptions, - ]) - - const filteredCommands = React.useMemo(() => { - if (!searchQuery) return baseCommands - - return baseCommands.map((group) => { - if (group.group === "Search Results") { - return group - } - - return { - ...group, - items: group.items.filter((command) => { - const searchString = [ - command.label, - command.subtitle, - command.description, - command.type, - ] - .filter(Boolean) - .join(" ") - .toLowerCase() - - return fuzzySearch(searchString, searchQuery.toLowerCase()) - }), - } - }).filter((group) => - group.items.length > 0 || group.group === "Search Results" - ) - }, [baseCommands, searchQuery]) return ( <> - - - + +
+ + + + +
+ + {isSearching ? ( - - Loading results... - - ) : filteredCommands.length > 0 ? ( - filteredCommands.map((group, groupIndex) => ( - - {group.items.map( - (command: CommandItemData, itemIndex: number) => ( - + Loading results... + + ) : ( + <> + {searchQuery && searchResults.length > 0 && ( + + {searchResults.map((snippet: Snippet) => ( + { - if (command.action) { - command.action() - } else if (command.href && !command.disabled) { - window.location.href = command.href - setOpen(false) - } + window.location.href = `/editor?snippet_id=${snippet.snippet_id}` + setOpen(false) }} - disabled={command.disabled} - className="flex items-center justify-between" + className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" >
- {command.label} - {command.subtitle && ( + + {snippet.name || snippet.unscoped_name} + + {snippet.description && ( - {command.subtitle} - - )} - {command.description && ( - - {command.description} + {snippet.description} )} + + Last edited: {new Date(snippet.updated_at).toLocaleDateString()} +
- - {command.type} - -
- ), - )} -
- )) - ) : ( - No results found. + snippet + + ))} + + )} + + {!searchQuery && recentSnippets.length > 0 && ( + + {recentSnippets.slice(0, 6).map((snippet) => ( + { + window.location.href = `/editor?snippet_id=${snippet.snippet_id}` + setOpen(false) + }} + className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" + > +
+ + {snippet.unscoped_name} + + + Last edited: {new Date(snippet.updated_at).toLocaleDateString()} + +
+ snippet +
+ ))} +
+ )} + + + {blankTemplates.map((template) => ( + { + if (!template.disabled) { + window.location.href = `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}` + setOpen(false) + } + }} + className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default disabled:opacity-50" + > + {template.name} + {template.type} + + ))} + + + + {templates.map((template) => ( + { + window.location.href = `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}` + setOpen(false) + }} + className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" + > + {template.name} + {template.type} + + ))} + + + + {importOptions.map((option) => ( + { + if (option.special) { + setOpen(false) + setIsJLCPCBDialogOpen(true) + } else { + setOpen(false) + toastNotImplemented(`${option.name} Import`) + } + }} + className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" + > + Import {option.name} + {option.type} + + ))} + + + {searchQuery && !searchResults.length && !isSearching && ( + + No results found. + + )} + )} -
-
+ + + ) } -export default CmdKMenu +export default CmdKMenu \ No newline at end of file From 26b1602d2f44a57ea6113b1a5a43ccb00fe928de Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Thu, 24 Oct 2024 20:40:40 +0530 Subject: [PATCH 11/12] removed timestamp from search results --- src/components/CmdKMenu.tsx | 64 +++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 9ef0f216..19c145f2 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -2,7 +2,7 @@ import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog" import { useAxios } from "@/hooks/use-axios" import { useGlobalStore } from "@/hooks/use-global-store" import { useNotImplementedToast } from "@/hooks/use-toast" -import { Command } from 'cmdk' +import { Command } from "cmdk" import { Snippet } from "fake-snippets-api/lib/db/schema" import React from "react" import { useQuery } from "react-query" @@ -41,7 +41,7 @@ const CmdKMenu = () => { }, { enabled: Boolean(searchQuery), - } + }, ) // Recent snippets query @@ -50,13 +50,13 @@ const CmdKMenu = () => { async () => { if (!currentUser) return [] const response = await axios.get<{ snippets: Snippet[] }>( - `/snippets/list?owner_name=${currentUser}` + `/snippets/list?owner_name=${currentUser}`, ) return response.data.snippets || [] }, { enabled: !!currentUser && !searchQuery, - } + }, ) React.useEffect(() => { @@ -109,7 +109,7 @@ const CmdKMenu = () => { d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> - { ) : ( <> {searchQuery && searchResults.length > 0 && ( - + {searchResults.map((snippet: Snippet) => ( { {snippet.description} )} - - Last edited: {new Date(snippet.updated_at).toLocaleDateString()} - snippet @@ -156,7 +156,10 @@ const CmdKMenu = () => { )} {!searchQuery && recentSnippets.length > 0 && ( - + {recentSnippets.slice(0, 6).map((snippet) => ( { {snippet.unscoped_name} - Last edited: {new Date(snippet.updated_at).toLocaleDateString()} + Last edited:{" "} + {new Date(snippet.updated_at).toLocaleDateString()} snippet @@ -181,7 +185,10 @@ const CmdKMenu = () => { )} - + {blankTemplates.map((template) => ( { }} className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default disabled:opacity-50" > - {template.name} - {template.type} + + {template.name} + + + {template.type} + ))} - + {templates.map((template) => ( { }} className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" > - {template.name} - {template.type} + + {template.name} + + + {template.type} + ))} - + {importOptions.map((option) => ( { }} className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" > - Import {option.name} + + Import {option.name} + {option.type} ))} @@ -254,9 +277,8 @@ const CmdKMenu = () => { open={isJLCPCBDialogOpen} onOpenChange={setIsJLCPCBDialogOpen} /> - ) } -export default CmdKMenu \ No newline at end of file +export default CmdKMenu From 33d5c8597a9f1a0f52529bb46ac222043ce1b494 Mon Sep 17 00:00:00 2001 From: Mrudul Patil Date: Wed, 30 Oct 2024 11:48:17 +0530 Subject: [PATCH 12/12] absolute position for cmdk dialog --- src/components/CmdKMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CmdKMenu.tsx b/src/components/CmdKMenu.tsx index 19c145f2..9329985d 100644 --- a/src/components/CmdKMenu.tsx +++ b/src/components/CmdKMenu.tsx @@ -93,7 +93,7 @@ const CmdKMenu = () => { open={open} onOpenChange={setOpen} label="Command Menu" - className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-w-2xl w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" + className="fixed top-32 left-1/2 -translate-x-1/2 max-w-2xl w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" >
{ />
- + {isSearching ? ( Loading results...