diff --git a/frontend/src/app/json-editor/page.tsx b/frontend/src/app/json-editor/page.tsx index 64ee461f5..48e6f2a8f 100644 --- a/frontend/src/app/json-editor/page.tsx +++ b/frontend/src/app/json-editor/page.tsx @@ -2,42 +2,68 @@ import type { z } from "zod"; +import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { CalendarIcon, Check, Clipboard, Download } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { useTheme } from "next-themes"; import { format } from "date-fns"; import { toast } from "sonner"; +import Image from "next/image"; import type { Category } from "@/lib/types"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Calendar } from "@/components/ui/calendar"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; +import { basePath } from "@/config/site-config"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { fetchCategories } from "@/lib/data"; import { cn } from "@/lib/utils"; import type { Script } from "./_schemas/schemas"; +import { ScriptItem } from "../scripts/_components/script-item"; import InstallMethod from "./_components/install-method"; import { ScriptSchema } from "./_schemas/schemas"; import Categories from "./_components/categories"; import Note from "./_components/note"; -import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs"; -import SyntaxHighlighter from "react-syntax-highlighter"; -import { ScriptItem } from "../scripts/_components/script-item"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { search } from "@/components/command-menu"; -import { basePath } from "@/config/site-config"; -import Image from "next/image"; -import { useTheme } from "next-themes"; +function search(scripts: Script[], query: string): Script[] { + const queryLower = query.toLowerCase().trim(); + const searchWords = queryLower.split(/\s+/).filter(Boolean); + + return scripts + .map((script) => { + const nameLower = script.name.toLowerCase(); + const descriptionLower = (script.description || "").toLowerCase(); + + let score = 0; + + for (const word of searchWords) { + if (nameLower.includes(word)) { + score += 10; + } + if (descriptionLower.includes(word)) { + score += 5; + } + } + + return { script, score }; + }) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 20) + .map(({ script }) => script); +} const initialScript: Script = { name: "", @@ -77,32 +103,32 @@ export default function JSONGenerator() { const selectedCategoryObj = useMemo( () => categories.find(cat => cat.id.toString() === selectedCategory), - [categories, selectedCategory] + [categories, selectedCategory], ); const allScripts = useMemo( () => categories.flatMap(cat => cat.scripts || []), - [categories] + [categories], ); const scripts = useMemo(() => { - const query = searchQuery.trim() + const query = searchQuery.trim(); if (query) { - return search(allScripts, query) + return search(allScripts, query); } if (selectedCategoryObj) { - return selectedCategoryObj.scripts || [] + return selectedCategoryObj.scripts || []; } - return [] + return []; }, [allScripts, selectedCategoryObj, searchQuery]); useEffect(() => { fetchCategories() .then(setCategories) - .catch((error) => console.error("Error fetching categories:", error)); + .catch(error => console.error("Error fetching categories:", error)); }, []); useEffect(() => { @@ -122,11 +148,14 @@ export default function JSONGenerator() { if (updated.type === "pve") { scriptPath = `tools/pve/${updated.slug}.sh`; - } else if (updated.type === "addon") { + } + else if (updated.type === "addon") { scriptPath = `tools/addon/${updated.slug}.sh`; - } else if (method.type === "alpine") { + } + else if (method.type === "alpine") { scriptPath = `${updated.type}/alpine-${updated.slug}.sh`; - } else { + } + else { scriptPath = `${updated.type}/${updated.slug}.sh`; } @@ -145,11 +174,13 @@ export default function JSONGenerator() { }, []); const handleCopy = useCallback(() => { - if (!isValid) toast.warning("JSON schema is invalid. Copying anyway."); + if (!isValid) + toast.warning("JSON schema is invalid. Copying anyway."); navigator.clipboard.writeText(JSON.stringify(script, null, 2)); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); - if (isValid) toast.success("Copied metadata to clipboard"); + if (isValid) + toast.success("Copied metadata to clipboard"); }, [script]); const importScript = (script: Script) => { @@ -166,11 +197,11 @@ export default function JSONGenerator() { setIsValid(true); setZodErrors(null); toast.success("Imported JSON successfully"); - } catch (error) { + } + catch (error) { toast.error("Failed to read or parse the JSON file."); } - - } + }; const handleFileImport = useCallback(() => { const input = document.createElement("input"); @@ -180,7 +211,8 @@ export default function JSONGenerator() { input.onchange = (e: Event) => { const target = e.target as HTMLInputElement; const file = target.files?.[0]; - if (!file) return; + if (!file) + return; const reader = new FileReader(); reader.onload = (event) => { @@ -189,7 +221,8 @@ export default function JSONGenerator() { const parsed = JSON.parse(content); importScript(parsed); toast.success("Imported JSON successfully"); - } catch (error) { + } + catch (error) { toast.error("Failed to read the JSON file."); } }; @@ -243,7 +276,10 @@ export default function JSONGenerator() {
{zodErrors.issues.map((error, index) => ( - {error.path.join(".")} -{error.message} + {error.path.join(".")} + {" "} + - + {error.message} ))}
@@ -270,7 +306,7 @@ export default function JSONGenerator() { onOpenChange={setIsImportDialogOpen} > - e.preventDefault()}> + e.preventDefault()}> Import existing script @@ -292,7 +328,7 @@ export default function JSONGenerator() { - {categories.map((category) => ( + {categories.map(category => ( {category.name} @@ -302,40 +338,44 @@ export default function JSONGenerator() { setSearchQuery(e.target.value)} + onChange={e => setSearchQuery(e.target.value)} /> - {!selectedCategory && !searchQuery ? ( -

- Select a category or search for a script -

- ) : scripts.length === 0 ? ( -

- No scripts found -

- ) : ( -
- {scripts.map(script => ( -
{ - importScript(script); - setIsImportDialogOpen(false); - }} - > - {script.name} -

{script.name}

-
- ))} -
- )} + {!selectedCategory && !searchQuery + ? ( +

+ Select a category or search for a script +

+ ) + : scripts.length === 0 + ? ( +

+ No scripts found +

+ ) + : ( +
+ {scripts.map(script => ( +
{ + importScript(script); + setIsImportDialogOpen(false); + }} + > + {script.name} +

{script.name}

+
+ ))} +
+ )} @@ -348,15 +388,19 @@ export default function JSONGenerator() {
- updateScript("name", e.target.value)} /> + updateScript("name", e.target.value)} />
- updateScript("slug", e.target.value)} /> + updateScript("slug", e.target.value)} />
@@ -366,7 +410,7 @@ export default function JSONGenerator() { updateScript("logo", e.target.value || null)} + onChange={e => updateScript("logo", e.target.value || null)} />
@@ -374,24 +418,28 @@ export default function JSONGenerator() { updateScript("config_path", e.target.value || "")} + onChange={e => updateScript("config_path", e.target.value || "")} />