forked from forkanization/Proxmox-arm64
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -3,13 +3,14 @@ import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { ScriptSchema, type Script } from "@/app/json-editor/_schemas/schemas";
|
||||
import { Metadata } from "@/lib/types";
|
||||
|
||||
console.log('Current directory: ' + process.cwd());
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const versionsFileName = "versions.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const fileNames = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName !== metadataFileName)
|
||||
.filter((fileName) => fileName !== metadataFileName && fileName !== versionsFileName);
|
||||
|
||||
describe.each(fileNames)("%s", async (fileName) => {
|
||||
let script: Script;
|
||||
@@ -20,6 +21,7 @@ describe.each(fileNames)("%s", async (fileName) => {
|
||||
script = JSON.parse(fileContent);
|
||||
})
|
||||
|
||||
|
||||
it("should have valid json according to script schema", () => {
|
||||
ScriptSchema.parse(script);
|
||||
});
|
||||
@@ -27,6 +29,8 @@ describe.each(fileNames)("%s", async (fileName) => {
|
||||
it("should have a corresponding script file", () => {
|
||||
script.install_methods.forEach((method) => {
|
||||
const scriptPath = path.resolve("..", method.script)
|
||||
//FIXME: Dose note account for new dir structure and files in /script/tools
|
||||
|
||||
assert(fs.stat(scriptPath), `Script file not found: ${scriptPath}`)
|
||||
})
|
||||
});
|
||||
@@ -40,7 +44,6 @@ describe(`${metadataFileName}`, async () => {
|
||||
const fileContent = await fs.readFile(filePath, encoding)
|
||||
metadata = JSON.parse(fileContent);
|
||||
})
|
||||
|
||||
it("should have valid json according to metadata schema", () => {
|
||||
// TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts
|
||||
assert(metadata.categories.length > 0);
|
||||
|
||||
@@ -7,6 +7,7 @@ export const dynamic = "force-static";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const versionFileName = "version.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getMetadata = async () => {
|
||||
@@ -18,7 +19,7 @@ const getMetadata = async () => {
|
||||
|
||||
const getScripts = async () => {
|
||||
const filePaths = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName !== metadataFileName)
|
||||
.filter((fileName) => fileName !== metadataFileName && fileName !== versionFileName)
|
||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
||||
|
||||
const scripts = await Promise.all(
|
||||
@@ -39,7 +40,7 @@ export async function GET() {
|
||||
const categories = metadata.categories
|
||||
.map((category) => {
|
||||
category.scripts = scripts.filter((script) =>
|
||||
script.categories.includes(category.id),
|
||||
script.categories?.includes(category.id),
|
||||
);
|
||||
return category;
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AppVersion } from "@/lib/types";
|
||||
import { error } from "console";
|
||||
import { promises as fs } from "fs";
|
||||
// import Error from "next/error";
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const versionsFileName = "versions.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getVersions = async () => {
|
||||
const filePath = path.resolve(jsonDir, versionsFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const versions: AppVersion[] = JSON.parse(fileContent);
|
||||
|
||||
const modifiedVersions = versions.map(version => {
|
||||
let newName = version.name;
|
||||
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, '');
|
||||
return { ...version, name: newName, date: new Date(version.date) };
|
||||
});
|
||||
|
||||
return modifiedVersions;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
||||
const versions = await getVersions();
|
||||
return NextResponse.json(versions);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const err = error as globalThis.Error;
|
||||
return NextResponse.json({
|
||||
name: err.name,
|
||||
message: err.message || "An unexpected error occurred",
|
||||
version: "No version found - Error"
|
||||
}, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -15,116 +15,136 @@ import { ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
|
||||
type NoteProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
// Restore focus after state update
|
||||
if (key === "text") {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
// Restore focus after state update
|
||||
if (key === "text") {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [script, setScript]);
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [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)}
|
||||
ref={(el) => {
|
||||
inputRefs.current[index] = el;
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Notes</h3>
|
||||
{script.notes.map((note, index) => (
|
||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||
))}
|
||||
</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 (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Notes</h3>
|
||||
{script.notes.map((note, index) => (
|
||||
<NoteItem key={index} note={note} index={index} />
|
||||
))}
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Note);
|
||||
const NoteItem = memo(
|
||||
({
|
||||
note,
|
||||
index,
|
||||
updateNote,
|
||||
removeNote,
|
||||
}: {
|
||||
note: Script["notes"][number];
|
||||
index: number;
|
||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||
removeNote: (index: number) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateNote(index, "text", e.target.value);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}, [index, updateNote]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border p-4 rounded">
|
||||
<Input
|
||||
placeholder="Note Text"
|
||||
value={note.text}
|
||||
onChange={handleTextChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<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';
|
||||
|
||||
|
||||
export default memo(Note);
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Script } from "@/lib/types";
|
||||
import { Script, AppVersion } from "@/lib/types";
|
||||
import { fetchVersions } from "@/lib/data";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -15,6 +17,8 @@ import InstallCommand from "./ScriptItems/InstallCommand";
|
||||
import InterFaces from "./ScriptItems/InterFaces";
|
||||
import Tooltips from "./ScriptItems/Tooltips";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
function ScriptItem({
|
||||
item,
|
||||
@@ -27,6 +31,23 @@ function ScriptItem({
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
const [versions, setVersions] = useState<AppVersion[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchVersions()
|
||||
.then((fetchedVersions) => {
|
||||
console.log("Fetched Versions: ", fetchedVersions);
|
||||
if (Array.isArray(fetchedVersions)) {
|
||||
setVersions(fetchedVersions);
|
||||
} else if (fetchedVersions && typeof fetchedVersions === "object") {
|
||||
setVersions([fetchedVersions]);
|
||||
} else {
|
||||
setVersions([]);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error fetching versions:", error));
|
||||
}, []);
|
||||
|
||||
const defaultInstallMethod = item.install_methods?.[0];
|
||||
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
|
||||
@@ -48,8 +69,8 @@ function ScriptItem({
|
||||
src={item.logo || `/${basePath}/logo.png`}
|
||||
width={400}
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
height={400}
|
||||
alt={item.name}
|
||||
@@ -71,6 +92,32 @@ function ScriptItem({
|
||||
<div className="flex gap-5">
|
||||
<DefaultSettings item={item} />
|
||||
</div>
|
||||
<div>{versions.length === 0 ? (<p>Loading versions...</p>) :
|
||||
(<>
|
||||
<p className="text-l text-foreground">Version:</p>
|
||||
<p className="text-l text-muted-foreground">{versions.find((v) =>
|
||||
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
|
||||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
|
||||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
|
||||
|
||||
)?.version || "No Version information found"
|
||||
}</p>
|
||||
<p className="text-l text-foreground">Latest Version changes(Pulled from newreleases.io):</p>
|
||||
<p className="text-l text-muted-foreground">
|
||||
{(() => {
|
||||
const matchedVersion = versions.find((v) =>
|
||||
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
|
||||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
|
||||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
|
||||
);
|
||||
return matchedVersion?.date ?
|
||||
extractDate(matchedVersion.date as unknown as string) :
|
||||
"No date information found"
|
||||
})()}
|
||||
</p>
|
||||
</>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,8 +41,8 @@ export const mostPopularScripts = [
|
||||
];
|
||||
|
||||
export const analytics = {
|
||||
url: "-",
|
||||
token: "b60d3032-1a11-4244-a100-81d26c5c49a7",
|
||||
url: "analytics.proxmoxve-scripts.com",
|
||||
token: "aefee1b9-2a12-4ac2-9d82-a63113edc62e",
|
||||
};
|
||||
|
||||
export const AlertColors = {
|
||||
|
||||
@@ -8,3 +8,11 @@ export const fetchCategories = async () => {
|
||||
const categories: Category[] = await response.json();
|
||||
return categories;
|
||||
};
|
||||
|
||||
export const fetchVersions = async () => {
|
||||
const response = await fetch(`api/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch versions: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -56,4 +56,10 @@ export interface Version {
|
||||
export interface OperatingSystem {
|
||||
name: string;
|
||||
versions: Version[];
|
||||
}
|
||||
|
||||
export interface AppVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
date: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user