mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-10 17:59:17 +00:00
Optimize website json-editor page and components (#265)
* Update mariadb.json * Update vaultwarden.json * Update vaultwarden.json * Update keycloak.json * Update json/keycloak.json Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update mariadb.json Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Add canonical link to layout for improved SEO and page indexing * Fix image source fallback for script logos to use a consistent relative path * Fix image source for script logos across components to consistently use the "/ProxmoxVE/logo.png" path * Update image source for script logos to use basePath for consistent paths across all components * Fix image source for script logos to ensure leading slash is consistent for all components' paths * Add JSON generator component with validation and UI elements for managing scripts, categories, and installation methods * Add calendar and label components; enhance JSON generator with date selection and script path updates for installation methods * Enhance Alerts component with dynamic colored notes using AlertColors from config for better visibility and consistency * Remove MultiSelect component * Update JSON generator: streamline install methods, enhance note type selection, and refine button behavior for better UX * Refactor AlertColors: unify warning and danger styles for consistency and improved visual hierarchy in alerts * Enhance JSONGenerator: improve SelectItem layout with color indicators for better visual representation of alert types * Refactor JSON schema definitions in JSONGenerator: separate InstallMethod and Note schemas for better structure and readability * Fix JSONGenerator: streamline SelectItem markup and enhance JSON display layout for improved readability and user experience * Refactor JSON schema handling: move schema definitions to separate file * Enhance error handling in JSONGenerator: display Zod validation errors on user input for better feedback and debugging * Export InstallMethodSchema and integrate into JSONGenerator for better validation of install method data input * Add Categories and Note components to JSONGenerator for better organization and modularity in the JSON editing interface * Remove unused imports * Add JSON Editor route to sitemap for improved SEO and navigation * Refactor JSON Editor components to improve performance with memoization and streamline state updates with useCallback --------- Co-authored-by: CanbiZ <47820557+MickLesk@users.noreply.github.com> Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
This commit is contained in:
parent
f6cc26af3a
commit
10fe784e1c
@ -10,6 +10,7 @@ import { Category } from "@/lib/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ScriptSchema } from "../_schemas/schemas";
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -21,7 +22,42 @@ type CategoryProps = {
|
|||||||
categories: Category[];
|
categories: Category[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Categories({
|
const CategoryTag = memo(({
|
||||||
|
category,
|
||||||
|
onRemove
|
||||||
|
}: {
|
||||||
|
category: Category;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) => (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
{category.name}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
|
||||||
|
onClick={onRemove}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
|
||||||
|
CategoryTag.displayName = 'CategoryTag';
|
||||||
|
|
||||||
|
function Categories({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
categories,
|
categories,
|
||||||
@ -40,64 +76,44 @@ export default function Categories({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div>
|
<Label>
|
||||||
<Label>
|
Category <span className="text-red-500">*</span>
|
||||||
Category <span className="text-red-500">*</span>
|
</Label>
|
||||||
</Label>
|
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
<SelectTrigger>
|
||||||
<SelectTrigger>
|
<SelectValue placeholder="Select a category" />
|
||||||
<SelectValue placeholder="Select a category" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{categories.map((category) => (
|
||||||
{categories.map((category) => (
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
{category.name}
|
||||||
{category.name}
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
"flex flex-wrap gap-2",
|
||||||
"flex flex-wrap gap-2",
|
script.categories.length !== 0 && "mt-2",
|
||||||
script.categories.length !== 0 && "mt-2",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{script.categories.map((categoryId) => {
|
||||||
{script.categories.map((categoryId) => {
|
const category = categoryMap.get(categoryId);
|
||||||
const category = categories.find((c) => c.id === categoryId);
|
return category ? (
|
||||||
return category ? (
|
<CategoryTag
|
||||||
<span
|
key={categoryId}
|
||||||
key={categoryId}
|
category={category}
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
onRemove={() => removeCategory(categoryId)}
|
||||||
>
|
/>
|
||||||
{category.name}
|
) : null;
|
||||||
<button
|
})}
|
||||||
type="button"
|
|
||||||
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
|
|
||||||
onClick={() => removeCategory(categoryId)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<svg
|
|
||||||
className="h-3 w-3"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Categories);
|
||||||
|
@ -10,6 +10,7 @@ import { PlusCircle, Trash2 } from "lucide-react";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -20,13 +21,13 @@ type InstallMethodProps = {
|
|||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InstallMethod({
|
function InstallMethod({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
setIsValid,
|
setIsValid,
|
||||||
setZodErrors,
|
setZodErrors,
|
||||||
}: InstallMethodProps) {
|
}: InstallMethodProps) {
|
||||||
const addInstallMethod = () => {
|
const addInstallMethod = useCallback(() => {
|
||||||
setScript((prev) => {
|
setScript((prev) => {
|
||||||
const method = InstallMethodSchema.parse({
|
const method = InstallMethodSchema.parse({
|
||||||
type: "default",
|
type: "default",
|
||||||
@ -44,9 +45,9 @@ export default function InstallMethod({
|
|||||||
install_methods: [...prev.install_methods, method],
|
install_methods: [...prev.install_methods, method],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
}, [setScript]);
|
||||||
|
|
||||||
const updateInstallMethod = (
|
const updateInstallMethod = useCallback((
|
||||||
index: number,
|
index: number,
|
||||||
key: keyof Script["install_methods"][number],
|
key: keyof Script["install_methods"][number],
|
||||||
value: Script["install_methods"][number][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;
|
return updated;
|
||||||
});
|
});
|
||||||
};
|
}, [setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
const removeInstallMethod = (index: number) => {
|
const removeInstallMethod = useCallback((index: number) => {
|
||||||
setScript((prev) => ({
|
setScript((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
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<HTMLInputElement>) => void;
|
||||||
|
type?: string;
|
||||||
|
}) => (
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
ResourceInput.displayName = 'ResourceInput';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -109,33 +131,33 @@ export default function InstallMethod({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="CPU in Cores"
|
placeholder="CPU in Cores"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.cpu || ""}
|
value={method.resources.cpu}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
cpu: e.target.value ? Number(e.target.value) : null,
|
cpu: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="RAM in MB"
|
placeholder="RAM in MB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.ram || ""}
|
value={method.resources.ram}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
ram: e.target.value ? Number(e.target.value) : null,
|
ram: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="HDD in GB"
|
placeholder="HDD in GB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.hdd || ""}
|
value={method.resources.hdd}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
hdd: e.target.value ? Number(e.target.value) : null,
|
hdd: e.target.value ? Number(e.target.value) : null,
|
||||||
@ -144,21 +166,21 @@ export default function InstallMethod({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="OS"
|
placeholder="OS"
|
||||||
value={method.resources.os || ""}
|
value={method.resources.os}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
os: e.target.value || null,
|
os: e.target.value || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<ResourceInput
|
||||||
placeholder="Version"
|
placeholder="Version"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.version || ""}
|
value={method.resources.version}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
version: e.target.value ? Number(e.target.value) : null,
|
version: e.target.value ? Number(e.target.value) : null,
|
||||||
@ -168,7 +190,7 @@ export default function InstallMethod({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size={"sm"}
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeInstallMethod(index)}
|
onClick={() => removeInstallMethod(index)}
|
||||||
>
|
>
|
||||||
@ -178,7 +200,7 @@ export default function InstallMethod({
|
|||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size={"sm"}
|
size="sm"
|
||||||
disabled={script.install_methods.length >= 2}
|
disabled={script.install_methods.length >= 2}
|
||||||
onClick={addInstallMethod}
|
onClick={addInstallMethod}
|
||||||
>
|
>
|
||||||
@ -187,3 +209,5 @@ export default function InstallMethod({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(InstallMethod);
|
||||||
|
@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ScriptSchema } from "../_schemas/schemas";
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
@ -21,95 +22,98 @@ type NoteProps = {
|
|||||||
setIsValid: (isValid: boolean) => void;
|
setIsValid: (isValid: boolean) => void;
|
||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
};
|
};
|
||||||
export default function Note({
|
|
||||||
|
function Note({
|
||||||
script,
|
script,
|
||||||
setScript,
|
setScript,
|
||||||
setIsValid,
|
setIsValid,
|
||||||
setZodErrors,
|
setZodErrors,
|
||||||
}: NoteProps) {
|
}: NoteProps) {
|
||||||
const addNote = () => {
|
const addNote = useCallback(() => {
|
||||||
const newScript: Script = {
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
notes: [...script.notes, { text: "", type: "" }],
|
notes: [...script.notes, { text: "", type: "" }],
|
||||||
};
|
});
|
||||||
setScript(newScript);
|
}, [script, setScript]);
|
||||||
};
|
|
||||||
|
|
||||||
const updateNote = (
|
const updateNote = useCallback((
|
||||||
index: number,
|
index: number,
|
||||||
key: keyof Script["notes"][number],
|
key: keyof Script["notes"][number],
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
const updated: Script = {
|
const updated: Script = {
|
||||||
...script,
|
...script,
|
||||||
notes: script.notes.map((note: Script["notes"][number], i: number) =>
|
notes: script.notes.map((note, i) =>
|
||||||
i === index ? { ...note, [key]: value } : note,
|
i === index ? { ...note, [key]: value } : note,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
const result = ScriptSchema.safeParse(updated);
|
const result = ScriptSchema.safeParse(updated);
|
||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
setZodErrors(result.success ? null : result.error);
|
||||||
setZodErrors(result.error);
|
|
||||||
} else {
|
|
||||||
setZodErrors(null);
|
|
||||||
}
|
|
||||||
setScript(updated);
|
setScript(updated);
|
||||||
};
|
}, [script, setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
const removeNote = (index: number) => {
|
const removeNote = useCallback((index: number) => {
|
||||||
const newScript: Script = {
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
notes: script.notes.filter((_: Script["notes"][number], i: number) => i !== index),
|
notes: script.notes.filter((_, i) => i !== index),
|
||||||
};
|
});
|
||||||
setScript(newScript);
|
}, [script, setScript]);
|
||||||
};
|
|
||||||
|
const NoteItem = memo(({ note, index }: { note: Script["notes"][number], index: number }) => (
|
||||||
|
<div className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={(e) => updateNote(index, "text", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={note.type}
|
||||||
|
onValueChange={(value) => updateNote(index, "type", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.keys(AlertColors).map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-4 rounded-full border",
|
||||||
|
AlertColors[type as keyof typeof AlertColors],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeNote(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
NoteItem.displayName = 'NoteItem';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-xl font-semibold">Notes</h3>
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
{script.notes.map((note, index) => (
|
{script.notes.map((note, index) => (
|
||||||
<div key={index} className="space-y-2 border p-4 rounded">
|
<NoteItem key={index} note={note} index={index} />
|
||||||
<Input
|
|
||||||
placeholder="Note Text"
|
|
||||||
value={note.text}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateNote(index, "text", e.target.value)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={note.type}
|
|
||||||
onValueChange={(value: string) => updateNote(index, "type", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="flex-1">
|
|
||||||
<SelectValue placeholder="Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.keys(AlertColors).map((type) => (
|
|
||||||
<SelectItem key={type} value={type}>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"size-4 rounded-full border",
|
|
||||||
AlertColors[type as keyof typeof AlertColors],
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
size={"sm"}
|
|
||||||
variant="destructive"
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeNote(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
<Button type="button" size={"sm"} onClick={addNote}>
|
<Button type="button" size="sm" onClick={addNote}>
|
||||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Note);
|
||||||
|
@ -20,7 +20,7 @@ import { Category } from "@/lib/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Categories from "./_components/Categories";
|
import Categories from "./_components/Categories";
|
||||||
@ -30,66 +30,98 @@ import { ScriptSchema } from "./_schemas/schemas";
|
|||||||
|
|
||||||
type Script = z.infer<typeof ScriptSchema>;
|
type Script = z.infer<typeof ScriptSchema>;
|
||||||
|
|
||||||
export default function JSONGenerator() {
|
const initialScript: Script = {
|
||||||
const [script, setScript] = useState<Script>({
|
name: "",
|
||||||
name: "",
|
slug: "",
|
||||||
slug: "",
|
categories: [],
|
||||||
categories: [],
|
date_created: "",
|
||||||
date_created: "",
|
type: "ct",
|
||||||
type: "ct",
|
updateable: false,
|
||||||
updateable: false,
|
privileged: false,
|
||||||
privileged: false,
|
interface_port: null,
|
||||||
interface_port: null,
|
documentation: null,
|
||||||
documentation: null,
|
website: null,
|
||||||
website: null,
|
logo: null,
|
||||||
logo: null,
|
description: "",
|
||||||
description: "",
|
install_methods: [],
|
||||||
install_methods: [],
|
default_credentials: {
|
||||||
default_credentials: {
|
username: null,
|
||||||
username: null,
|
password: null,
|
||||||
password: null,
|
},
|
||||||
},
|
notes: [],
|
||||||
notes: [],
|
};
|
||||||
});
|
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
|
||||||
|
|
||||||
|
export default function JSONGenerator() {
|
||||||
|
const [script, setScript] = useState<Script>(initialScript);
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const [isValid, setIsValid] = useState(false);
|
const [isValid, setIsValid] = useState(false);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then((data) => {
|
.then(setCategories)
|
||||||
setCategories(data);
|
|
||||||
})
|
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch((error) => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateScript = (key: keyof Script, value: Script[keyof Script]) => {
|
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||||
setScript((prev) => {
|
setScript((prev) => {
|
||||||
const updated = { ...prev, [key]: value };
|
const updated = { ...prev, [key]: value };
|
||||||
|
|
||||||
// Update script paths for install methods if `type` or `slug` changed
|
|
||||||
if (key === "type" || key === "slug") {
|
if (key === "type" || key === "slug") {
|
||||||
updated.install_methods = updated.install_methods.map((method) => ({
|
updated.install_methods = updated.install_methods.map((method) => ({
|
||||||
...method,
|
...method,
|
||||||
script:
|
script: method.type === "alpine"
|
||||||
method.type === "alpine"
|
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
: `/${updated.type}/${updated.slug}.sh`,
|
||||||
: `/${updated.type}/${updated.slug}.sh`,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = ScriptSchema.safeParse(updated);
|
const result = ScriptSchema.safeParse(updated);
|
||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
setZodErrors(result.success ? null : result.error);
|
||||||
setZodErrors(result.error);
|
|
||||||
} else {
|
|
||||||
setZodErrors(null);
|
|
||||||
}
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
toast.success("Copied metadata to clipboard");
|
||||||
|
}, [script]);
|
||||||
|
|
||||||
|
const handleDateSelect = useCallback((date: Date | undefined) => {
|
||||||
|
updateScript(
|
||||||
|
"date_created",
|
||||||
|
format(date || new Date(), "yyyy-MM-dd")
|
||||||
|
);
|
||||||
|
}, [updateScript]);
|
||||||
|
|
||||||
|
const formattedDate = useMemo(() =>
|
||||||
|
script.date_created ? format(script.date_created, "PPP") : undefined,
|
||||||
|
[script.date_created]
|
||||||
|
);
|
||||||
|
|
||||||
|
const validationAlert = useMemo(() => (
|
||||||
|
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
|
||||||
|
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{isValid
|
||||||
|
? "The current JSON is valid according to the schema."
|
||||||
|
: "The current JSON does not match the required schema."}
|
||||||
|
</AlertDescription>
|
||||||
|
{zodErrors && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{zodErrors.errors.map((error, index) => (
|
||||||
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
|
{error.path.join(".")} - {error.message}
|
||||||
|
</AlertDescription>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
), [isValid, zodErrors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen mt-20">
|
<div className="flex h-screen mt-20">
|
||||||
@ -155,11 +187,7 @@ export default function JSONGenerator() {
|
|||||||
!script.date_created && "text-muted-foreground",
|
!script.date_created && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{script.date_created ? (
|
{formattedDate || <span>Pick a date</span>}
|
||||||
format(script.date_created, "PPP")
|
|
||||||
) : (
|
|
||||||
<span>Pick a date</span>
|
|
||||||
)}
|
|
||||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@ -167,12 +195,7 @@ export default function JSONGenerator() {
|
|||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={new Date(script.date_created)}
|
selected={new Date(script.date_created)}
|
||||||
onSelect={(date) =>
|
onSelect={handleDateSelect}
|
||||||
updateScript(
|
|
||||||
"date_created",
|
|
||||||
format(date || new Date(), "yyyy-MM-dd"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
initialFocus
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@ -199,18 +222,14 @@ export default function JSONGenerator() {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.updateable}
|
checked={script.updateable}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => updateScript("updateable", checked)}
|
||||||
updateScript("updateable", checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label>Updateable</label>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.privileged}
|
checked={script.privileged}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => updateScript("privileged", checked)}
|
||||||
updateScript("privileged", checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label>Privileged</label>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
@ -219,12 +238,7 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
value={script.interface_port || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||||
updateScript(
|
|
||||||
"interface_port",
|
|
||||||
e.target.value ? Number(e.target.value) : null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
@ -235,9 +249,7 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||||
updateScript("documentation", e.target.value || null)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod
|
<InstallMethod
|
||||||
@ -250,22 +262,18 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("default_credentials", {
|
||||||
updateScript("default_credentials", {
|
...script.default_credentials,
|
||||||
...script.default_credentials,
|
username: e.target.value || null,
|
||||||
username: e.target.value || null,
|
})}
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateScript("default_credentials", {
|
||||||
updateScript("default_credentials", {
|
...script.default_credentials,
|
||||||
...script.default_credentials,
|
password: e.target.value || null,
|
||||||
password: e.target.value || null,
|
})}
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note
|
<Note
|
||||||
script={script}
|
script={script}
|
||||||
@ -276,36 +284,13 @@ export default function JSONGenerator() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||||
<Alert
|
{validationAlert}
|
||||||
className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}
|
|
||||||
>
|
|
||||||
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{isValid
|
|
||||||
? "The current JSON is valid according to the schema."
|
|
||||||
: "The current JSON does not match the required schema."}
|
|
||||||
</AlertDescription>
|
|
||||||
{zodErrors && (
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
{zodErrors.errors.map((error, index) => (
|
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
|
||||||
{error.path.join(".")} - {error.message}
|
|
||||||
</AlertDescription>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-2 top-2"
|
className="absolute right-2 top-2"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={handleCopy}
|
||||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
|
||||||
setIsCopied(true);
|
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
|
||||||
toast.success("Copied metadata to clipboard");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
|
Loading…
Reference in New Issue
Block a user