Refactor all components to use data from new API

This commit is contained in:
Bram Suurd 2024-11-06 17:25:02 +01:00
parent 1ef3c5a0eb
commit 76cf85293c
11 changed files with 207 additions and 222 deletions

View File

@ -42,10 +42,10 @@ export default function ScriptAccordion({
useEffect(() => {
if (selectedScript) {
const category = items.find((category) =>
category.expand.items.some((script) => script.title === selectedScript),
category.scripts.some((script) => script.name === selectedScript),
);
if (category) {
setExpandedItem(category.catagoryName);
setExpandedItem(category.name);
handleSelected(selectedScript);
}
}
@ -60,81 +60,80 @@ export default function ScriptAccordion({
{items.map((category) => (
<AccordionItem
key={category.id + ":category"}
value={category.catagoryName}
value={category.name}
className={cn("sm:text-md flex flex-col border-none", {
"rounded-lg bg-accent/30": expandedItem === category.catagoryName,
"rounded-lg bg-accent/30": expandedItem === category.name,
})}
>
<AccordionTrigger
className={cn(
"duration-250 rounded-lg transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:bg-accent",
{ "": expandedItem === category.catagoryName },
{ "": expandedItem === category.name },
)}
>
<div className="mr-2 flex w-full items-center justify-between">
<span className="pl-2">{category.catagoryName} </span>
<span className="pl-2">{category.name} </span>
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
{category.expand.items.length}
{category.scripts.length}
</span>
</div>{" "}
</AccordionTrigger>
<AccordionContent
data-state={
expandedItem === category.catagoryName ? "open" : "closed"
expandedItem === category.name ? "open" : "closed"
}
className="pt-0"
>
{category.expand.items
{category.scripts
.slice()
.sort((a, b) => a.title.localeCompare(b.title))
.sort((a, b) => a.name.localeCompare(b.name))
.map((script, index) => (
<div key={index}>
<Link
href={{
pathname: "/scripts",
query: { id: script.title },
query: { id: script.name},
}}
prefetch={false}
className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${
selectedScript === script.title
selectedScript === script.name
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
: ""
}`}
onClick={() => handleSelected(script.title)}
onClick={() => handleSelected(script.name)}
ref={(el) => {
linkRefs.current[script.title] = el;
linkRefs.current[script.name] = el;
}}
>
<Image
src={script.logo}
height={16}
width={16}
unoptimized
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
"/logo.png")
}
alt={script.title}
className="mr-1 w-4 h-4 rounded-full"
/>
<span className="flex items-center gap-2">
{script.title}
{script.isMostViewed && (
<Star className="h-3 w-3 text-yellow-500"></Star>
)}
</span>
<div className="flex items-center">
<Image
src={script.logo || "/logo.png"}
height={16}
width={16}
unoptimized
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
"/logo.png")
}
alt={script.name}
className="mr-1 w-4 h-4 rounded-full"
/>
<span className="flex items-center gap-2">
{script.name}
</span>
</div>
<Badge
className={cn(
"ml-auto w-[37.69px] justify-center text-center",
{
"text-primary/75": script.item_type === "VM",
"text-yellow-500/75": script.item_type === "LXC",
"border-none": script.item_type === "",
hidden: !["VM", "LXC", ""].includes(script.item_type),
"text-primary/75": script.type === "vm",
"text-yellow-500/75": script.type === "ct",
"border-none": script.type === "misc",
hidden: !["VM", "LXC", ""].includes(script.type),
},
)}
>
{script.item_type}
{script.type}
</Badge>
</Link>
</div>

View File

@ -21,9 +21,9 @@ export function LatestScripts({ items }: { items: Category[] }) {
const latestScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.expand.items || []);
const scripts = items.flatMap((category) => category.scripts || []);
return scripts.sort(
(a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
);
}, [items]);
@ -70,14 +70,14 @@ export function LatestScripts({ items }: { items: Category[] }) {
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{latestScripts.slice(startIndex, endIndex).map((item) => (
<Card
key={item.id}
key={item.name}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
src={item.logo}
src={item.logo || "/logo.png"}
unoptimized
height={64}
width={64}
@ -87,11 +87,11 @@ export function LatestScripts({ items }: { items: Category[] }) {
</div>
<div className="flex flex-col">
<p className="text-lg line-clamp-1">
{item.title} {item.item_type}
{item.name} {item.type}
</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<CalendarPlus className="h-4 w-4" />
{extractDate(item.created)}
{extractDate(item.date_created)}
</p>
</div>
</CardTitle>
@ -106,7 +106,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
<Link
href={{
pathname: "/scripts",
query: { id: item.title },
query: { id: item.name },
}}
>
View Script
@ -120,100 +120,102 @@ export function LatestScripts({ items }: { items: Category[] }) {
);
}
export function MostViewedScripts({ items }: { items: Category[] }) {
const [page, setPage] = useState(1);
// TODO: find way to determine what the most popular scripts are
const mostViewedScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.expand.items || []);
const mostViewedScripts = scripts
.filter((script) => script.isMostViewed)
.map((script) => ({
...script,
}));
return mostViewedScripts;
}, [items]);
// export function MostViewedScripts({ items }: { items: Category[] }) {
// const [page, setPage] = useState(1);
const goToNextPage = () => {
setPage((prevPage) => prevPage + 1);
};
// const mostViewedScripts = useMemo(() => {
// if (!items) return [];
// const scripts = items.flatMap((category) => category.scripts || []);
// const mostViewedScripts = scripts
// .filter((script) => script.isMostViewed)
// .map((script) => ({
// ...script,
// }));
// return mostViewedScripts;
// }, [items]);
const goToPreviousPage = () => {
setPage((prevPage) => prevPage - 1);
};
// const goToNextPage = () => {
// setPage((prevPage) => prevPage + 1);
// };
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = page * ITEMS_PER_PAGE;
// const goToPreviousPage = () => {
// setPage((prevPage) => prevPage - 1);
// };
return (
<div className="">
{mostViewedScripts.length > 0 && (
<>
<h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
</>
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{mostViewedScripts.slice(startIndex, endIndex).map((item) => (
<Card
key={item.id}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex max-h-16 min-h-16 min-w-16 max-w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
unoptimized
src={item.logo}
height={64}
width={64}
alt=""
className="h-11 w-11 object-contain"
/>
</div>
<div className="flex flex-col">
<p className="line-clamp-1 text-lg">
{item.title} {item.item_type}
</p>
<p className="flex items-center gap-1 text-sm text-muted-foreground">
<CalendarPlus className="h-4 w-4" />
{extractDate(item.created)}
</p>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground break-words">
{item.description}
</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
<Link
href={{
pathname: "/scripts",
query: { id: item.title },
}}
prefetch={false}
>
View Script
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
<div className="flex justify-end gap-1 p-2">
{page > 1 && (
<Button onClick={goToPreviousPage} variant="outline">
Previous
</Button>
)}
{endIndex < mostViewedScripts.length && (
<Button onClick={goToNextPage} variant="outline">
{page === 1 ? "More.." : "Next"}
</Button>
)}
</div>
</div>
);
}
// const startIndex = (page - 1) * ITEMS_PER_PAGE;
// const endIndex = page * ITEMS_PER_PAGE;
// return (
// <div className="">
// {mostViewedScripts.length > 0 && (
// <>
// <h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
// </>
// )}
// <div className="min-w flex w-full flex-row flex-wrap gap-4">
// {mostViewedScripts.slice(startIndex, endIndex).map((item) => (
// <Card
// key={item.id}
// className="min-w-[250px] flex-1 flex-grow bg-accent/30"
// >
// <CardHeader>
// <CardTitle className="flex items-center gap-3">
// <div className="flex max-h-16 min-h-16 min-w-16 max-w-16 items-center justify-center rounded-lg bg-accent p-1">
// <Image
// unoptimized
// src={item.logo}
// height={64}
// width={64}
// alt=""
// className="h-11 w-11 object-contain"
// />
// </div>
// <div className="flex flex-col">
// <p className="line-clamp-1 text-lg">
// {item.title} {item.item_type}
// </p>
// <p className="flex items-center gap-1 text-sm text-muted-foreground">
// <CalendarPlus className="h-4 w-4" />
// {extractDate(item.created)}
// </p>
// </div>
// </CardTitle>
// </CardHeader>
// <CardContent>
// <CardDescription className="line-clamp-3 text-card-foreground break-words">
// {item.description}
// </CardDescription>
// </CardContent>
// <CardFooter className="">
// <Button asChild variant="outline">
// <Link
// href={{
// pathname: "/scripts",
// query: { id: item.title },
// }}
// prefetch={false}
// >
// View Script
// </Link>
// </Button>
// </CardFooter>
// </Card>
// ))}
// </div>
// <div className="flex justify-end gap-1 p-2">
// {page > 1 && (
// <Button onClick={goToPreviousPage} variant="outline">
// Previous
// </Button>
// )}
// {endIndex < mostViewedScripts.length && (
// <Button onClick={goToNextPage} variant="outline">
// {page === 1 ? "More.." : "Next"}
// </Button>
// )}
// </div>
// </div>
// );
// }

View File

@ -39,21 +39,21 @@ function ScriptItem({
<div className="flex">
<Image
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
src={item.logo}
src={item.logo || "/logo.png"}
width={400}
onError={(e) =>
((e.currentTarget as HTMLImageElement).src = "/logo.png")
}
height={400}
alt={item.title}
alt={item.name}
unoptimized
/>
<div className="ml-4 flex flex-col justify-between">
<div className="flex h-full w-full flex-col justify-between">
<div>
<h1 className="text-lg font-semibold">{item.title}</h1>
<h1 className="text-lg font-semibold">{item.name}</h1>
<p className="w-full text-sm text-muted-foreground">
Date added: {extractDate(item.created)}
Date added: {extractDate(item.date_created)}
</p>
</div>
<div className="flex gap-5">
@ -76,7 +76,7 @@ function ScriptItem({
<div className="mt-4 rounded-lg border bg-accent/50">
<div className="flex gap-3 px-4 py-2">
<h2 className="text-lg font-semibold">
How to {item.item_type ? "install" : "use"}
How to {item.type ? "install" : "use"}
</h2>
<Tooltips item={item} />
</div>

View File

@ -5,12 +5,12 @@ import { Info } from "lucide-react";
export default function Alerts({ item }: { item: Script }) {
return (
<>
{item.expand?.alerts?.length > 0 &&
item.expand.alerts.map((alert: any, index: number) => (
{item?.notes?.length > 0 &&
item.notes.map((note: any, index: number) => (
<div key={index} className="mt-4 flex flex-col gap-2">
<p className="inline-flex items-center gap-2 rounded-lg border border-red-500/25 bg-destructive/25 p-2 pl-4 text-sm">
<Info className="h-4 min-h-4 w-4 min-w-4" />
<span>{TextCopyBlock(alert.content)}</span>
<span>{TextCopyBlock(note.text)}</span>
</p>
</div>
))}

View File

@ -21,12 +21,12 @@ export default function Buttons({ item }: { item: Script }) {
};
const sourceUrl = useMemo(() => {
if (item.installCommand) {
const match = item.installCommand.match(pattern);
if (item.install_methods[0]?.script) {
const match = item.install_methods[0].script.match(pattern);
return match ? transformUrlToInstallScript(match[0]) : null;
}
return null;
}, [item.installCommand, pattern]);
}, [item.install_methods, pattern]);
return (
<div className="flex flex-wrap justify-end gap-2">
@ -49,17 +49,7 @@ export default function Buttons({ item }: { item: Script }) {
</Link>
</Button>
)}
{item.post_install && (
<Button variant="secondary" asChild>
<Link target="_blank" href={item.post_install}>
<span className="flex items-center gap-2">
<ExternalLink className="h-4 w-4" />
Post Install
</span>
</Link>
</Button>
)}
{item.installCommand && sourceUrl && (
{item.install_methods[0]?.script && sourceUrl && (
<Button variant="secondary" asChild>
<Link target="_blank" href={transformUrlToInstallScript(sourceUrl)}>
<span className="flex items-center gap-2">

View File

@ -4,7 +4,7 @@ import handleCopy from "@/components/handleCopy";
import { Script } from "@/lib/types";
export default function DefaultPassword({ item }: { item: Script }) {
const hasDefaultLogin = item?.expand?.default_login !== undefined;
const hasDefaultLogin = item.default_credentials.username != null && item.default_credentials.password != null;
return (
<div>
@ -17,7 +17,7 @@ export default function DefaultPassword({ item }: { item: Script }) {
<div className="flex flex-col gap-2 p-4">
<p className="mb-2 text-sm">
You can use the following credentials to login to the {""}
{item.title} {item.item_type}.
{item.name} {item.type}.
</p>
<div className="text-sm">
Username:{" "}
@ -25,10 +25,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
variant={"secondary"}
size={"null"}
onClick={() =>
handleCopy("username", item.expand.default_login.username)
handleCopy("username", item.default_credentials.username ?? "")
}
>
{item.expand.default_login.username}
{item.default_credentials.username}
</Button>
</div>
<div className="text-sm">
@ -37,10 +37,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
variant={"secondary"}
size={"null"}
onClick={() =>
handleCopy("password", item.expand.default_login.password)
handleCopy("password", item.default_credentials.password ?? "")
}
>
{item.expand.default_login.password}
{item.default_credentials.password}
</Button>
</div>
</div>

View File

@ -1,35 +1,41 @@
import { Script } from "@/lib/types";
export default function DefaultSettings({ item }: { item: Script }) {
const hasAlpineScript = item?.expand?.alpine_script !== undefined;
const defaultSettings = item.install_methods.find(
(method) => method.type === "default",
);
const defaultAlpineSettings = item.install_methods.find(
(method) => method.type === "alpine",
);
return (
<>
{item.default_cpu && (
{defaultSettings && (
<div>
<h2 className="text-md font-semibold">Default settings</h2>
<p className="text-sm text-muted-foreground">
CPU: {item.default_cpu}
CPU: {defaultSettings.resources.cpu}vCPU
</p>
<p className="text-sm text-muted-foreground">
RAM: {item.default_ram}
RAM: {defaultSettings.resources.ram}MB
</p>
<p className="text-sm text-muted-foreground">
HDD: {item.default_hdd}
HDD: {defaultSettings.resources.hdd}GB
</p>
</div>
)}
{hasAlpineScript && (
{defaultAlpineSettings && (
<div>
<h2 className="text-md font-semibold">Default Alpine settings</h2>
<p className="text-sm text-muted-foreground">
CPU: {item.expand.alpine_script.default_cpu}
CPU: {defaultAlpineSettings?.resources.cpu}vCPU
</p>
<p className="text-sm text-muted-foreground">
RAM: {item.expand.alpine_script.default_ram}
RAM: {defaultAlpineSettings?.resources.ram}MB
</p>
<p className="text-sm text-muted-foreground">
HDD: {item.expand.alpine_script.default_hdd}
HDD: {defaultAlpineSettings?.resources.hdd}GB
</p>
</div>
)}

View File

@ -2,32 +2,41 @@ import CodeCopyButton from "@/components/ui/code-copy-button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Script } from "@/lib/types";
const generateInstallCommand = (script: string) => {
return `bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/${script})"`;
}
export default function InstallCommand({ item }: { item: Script }) {
const { title, item_type, installCommand, expand } = item;
const hasAlpineScript = expand?.alpine_script !== undefined;
const alpineScript = item.install_methods.find(
(method) => method.type === "alpine",
);
const defaultScript = item.install_methods.find(
(method) => method.type === "default"
);
const renderInstructions = (isAlpine = false) => (
<>
<p className="text-sm mt-2">
{isAlpine ? (
<>
As an alternative option, you can use Alpine Linux and the {title}{" "}
package to create a {title} {item_type} container with faster
As an alternative option, you can use Alpine Linux and the {item.name}{" "}
package to create a {item.name} {item.type} container with faster
creation time and minimal system resource usage. You are also
obliged to adhere to updates provided by the package maintainer.
</>
) : item_type ? (
) : item.type ? (
<>
To create a new Proxmox VE {title} {item_type}, run the command
To create a new Proxmox VE {item.name} {item.type}, run the command
below in the Proxmox VE Shell.
</>
) : (
<>To use the {title} script, run the command below in the shell.</>
<>To use the {item.name} script, run the command below in the shell.</>
)}
</p>
{isAlpine && (
<p className="mt-2 text-sm">
To create a new Proxmox VE Alpine-{title} {item_type}, run the command
To create a new Proxmox VE Alpine-{item.name} {item.type}, run the command
below in the Proxmox VE Shell
</p>
)}
@ -36,7 +45,7 @@ export default function InstallCommand({ item }: { item: Script }) {
return (
<div className="p-4">
{hasAlpineScript ? (
{alpineScript ? (
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
<TabsList>
<TabsTrigger value="default">Default</TabsTrigger>
@ -44,25 +53,23 @@ export default function InstallCommand({ item }: { item: Script }) {
</TabsList>
<TabsContent value="default">
{renderInstructions()}
<CodeCopyButton>{installCommand}</CodeCopyButton>
<CodeCopyButton>{defaultScript?.script}</CodeCopyButton>
</TabsContent>
<TabsContent value="alpine">
{expand.alpine_script && (
<>
{renderInstructions(true)}
<CodeCopyButton>
{expand.alpine_script.installCommand}
</CodeCopyButton>
</>
)}
{renderInstructions(true)}
<CodeCopyButton>
{generateInstallCommand(alpineScript.script)}
</CodeCopyButton>
</TabsContent>
</Tabs>
) : (
) : defaultScript?.script ? (
<>
{renderInstructions()}
{installCommand && <CodeCopyButton>{installCommand}</CodeCopyButton>}
<CodeCopyButton>
{generateInstallCommand(defaultScript.script)}
</CodeCopyButton>
</>
)}
) : null}
</div>
);
}

View File

@ -37,11 +37,11 @@ export default function Tooltips({ item }: { item: Script }) {
content="This script will be run in a privileged LXC"
/>
)}
{item.isUpdateable && (
{item.updateable && (
<TooltipBadge
variant="success"
label="Updateable"
content={`To Update ${item.title}, run the command below (or type update) in the LXC Console.`}
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
/>
)}
</div>

View File

@ -18,7 +18,7 @@ const Sidebar = ({
<h1 className="text-xl font-bold">Categories</h1>
<p className="text-xs italic text-muted-foreground">
{items.reduce(
(acc, category) => acc + category.expand.items.length,
(acc, category) => acc + category.scripts.length,
0,
)}{" "}
Total scripts

View File

@ -10,7 +10,7 @@ import Sidebar from "./_components/Sidebar";
import { useQueryState } from "nuqs";
import {
LatestScripts,
MostViewedScripts,
// MostViewedScripts,
} from "./_components/ScriptInfoBlocks";
function ScriptContent() {
@ -21,39 +21,20 @@ function ScriptContent() {
useEffect(() => {
if (selectedScript && links.length > 0) {
const script = links
.map((category) => category.expand.items)
.map((category) => category.scripts)
.flat()
.find((script) => script.title === selectedScript);
.find((script) => script.name === selectedScript);
setItem(script);
}
}, [selectedScript, links]);
const sortCategories = (categories: Category[]): Category[] => {
return categories.sort((a: Category, b: Category) => {
if (
a.catagoryName === "Proxmox VE Tools" &&
b.catagoryName !== "Proxmox VE Tools"
) {
return -1;
} else if (
a.catagoryName !== "Proxmox VE Tools" &&
b.catagoryName === "Proxmox VE Tools"
) {
return 1;
} else {
return a.catagoryName.localeCompare(b.catagoryName);
}
});
};
useEffect(() => {
fetch(
`api/categories?_=${process.env.NEXT_PUBLIC_BUILD_TIME || Date.now()}`,
`api/categories`,
)
.then((response) => response.json())
.then((categories) => {
const sortedCategories = sortCategories(categories);
setLinks(sortedCategories);
setLinks(categories);
})
.catch((error) => console.error(error));
}, []);
@ -74,7 +55,7 @@ function ScriptContent() {
) : (
<div className="flex w-full flex-col gap-5">
<LatestScripts items={links} />
<MostViewedScripts items={links} />
{/* <MostViewedScripts items={links} /> */}
</div>
)}
</div>