diff --git a/frontend/src/app/json-editor/_components/Categories.tsx b/frontend/src/app/json-editor/_components/Categories.tsx index e3f4c61c..f214a94f 100644 --- a/frontend/src/app/json-editor/_components/Categories.tsx +++ b/frontend/src/app/json-editor/_components/Categories.tsx @@ -10,6 +10,7 @@ import { Category } from "@/lib/types"; import { cn } from "@/lib/utils"; import { z } from "zod"; import { ScriptSchema } from "../_schemas/schemas"; +import { memo } from "react"; type Script = z.infer; @@ -21,7 +22,42 @@ type CategoryProps = { categories: Category[]; }; -export default function Categories({ +const CategoryTag = memo(({ + category, + onRemove +}: { + category: Category; + onRemove: () => void; +}) => ( + + {category.name} + + +)); + +CategoryTag.displayName = 'CategoryTag'; + +function Categories({ script, setScript, categories, @@ -40,64 +76,44 @@ export default function Categories({ }); }; + const categoryMap = new Map(categories.map(c => [c.id, c])); + return ( - <> -
- - -
- {script.categories.map((categoryId) => { - const category = categories.find((c) => c.id === categoryId); - return category ? ( - - {category.name} - - - ) : null; - })} -
+
+ + +
+ {script.categories.map((categoryId) => { + const category = categoryMap.get(categoryId); + return category ? ( + removeCategory(categoryId)} + /> + ) : null; + })}
- +
); } + +export default memo(Categories); diff --git a/frontend/src/app/json-editor/_components/InstallMethod.tsx b/frontend/src/app/json-editor/_components/InstallMethod.tsx index df0ef66e..9a0a035b 100644 --- a/frontend/src/app/json-editor/_components/InstallMethod.tsx +++ b/frontend/src/app/json-editor/_components/InstallMethod.tsx @@ -10,6 +10,7 @@ import { PlusCircle, Trash2 } from "lucide-react"; import { Input } from "@/components/ui/input"; import { z } from "zod"; import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas"; +import { memo, useCallback } from "react"; type Script = z.infer; @@ -20,13 +21,13 @@ type InstallMethodProps = { setZodErrors: (zodErrors: z.ZodError | null) => void; }; -export default function InstallMethod({ +function InstallMethod({ script, setScript, setIsValid, setZodErrors, }: InstallMethodProps) { - const addInstallMethod = () => { + const addInstallMethod = useCallback(() => { setScript((prev) => { const method = InstallMethodSchema.parse({ type: "default", @@ -44,9 +45,9 @@ export default function InstallMethod({ install_methods: [...prev.install_methods, method], }; }); - }; + }, [setScript]); - const updateInstallMethod = ( + const updateInstallMethod = useCallback(( index: number, key: keyof Script["install_methods"][number], value: Script["install_methods"][number][keyof Script["install_methods"][number]], @@ -82,14 +83,35 @@ export default function InstallMethod({ } return updated; }); - }; + }, [setScript, setIsValid, setZodErrors]); - const removeInstallMethod = (index: number) => { + const removeInstallMethod = useCallback((index: number) => { setScript((prev) => ({ ...prev, install_methods: prev.install_methods.filter((_, i) => i !== index), })); - }; + }, [setScript]); + + const ResourceInput = memo(({ + placeholder, + value, + onChange, + type = "text" + }: { + placeholder: string; + value: string | number | null; + onChange: (e: React.ChangeEvent) => void; + type?: string; + }) => ( + + )); + + ResourceInput.displayName = 'ResourceInput'; return ( <> @@ -109,33 +131,33 @@ export default function InstallMethod({
- ) => + value={method.resources.cpu} + onChange={(e) => updateInstallMethod(index, "resources", { ...method.resources, cpu: e.target.value ? Number(e.target.value) : null, }) } /> - ) => + value={method.resources.ram} + onChange={(e) => updateInstallMethod(index, "resources", { ...method.resources, ram: e.target.value ? Number(e.target.value) : null, }) } /> - ) => + value={method.resources.hdd} + onChange={(e) => updateInstallMethod(index, "resources", { ...method.resources, hdd: e.target.value ? Number(e.target.value) : null, @@ -144,21 +166,21 @@ export default function InstallMethod({ />
- ) => + value={method.resources.os} + onChange={(e) => updateInstallMethod(index, "resources", { ...method.resources, os: e.target.value || null, }) } /> - ) => + value={method.resources.version} + onChange={(e) => updateInstallMethod(index, "resources", { ...method.resources, version: e.target.value ? Number(e.target.value) : null, @@ -168,7 +190,7 @@ export default function InstallMethod({
+
+ )); + + NoteItem.displayName = 'NoteItem'; return ( <>

Notes

{script.notes.map((note, index) => ( -
- ) => updateNote(index, "text", e.target.value)} - /> - - -
+ ))} - ); } + +export default memo(Note); diff --git a/frontend/src/app/json-editor/page.tsx b/frontend/src/app/json-editor/page.tsx index 144478b0..74708127 100644 --- a/frontend/src/app/json-editor/page.tsx +++ b/frontend/src/app/json-editor/page.tsx @@ -20,7 +20,7 @@ import { Category } from "@/lib/types"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; import { CalendarIcon, Check, Clipboard } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; import Categories from "./_components/Categories"; @@ -30,66 +30,98 @@ import { ScriptSchema } from "./_schemas/schemas"; type Script = z.infer; -export default function JSONGenerator() { - const [script, setScript] = useState