forked from forkanization/Proxmox-arm64
Remove JSON and frontend folders
To shrink size of PR to make GitHub not lag
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Bram Suurd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "@/styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
config.resolve.alias.canvas = false;
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
env: {
|
||||
BASE_PATH: "ProxmoxVE",
|
||||
},
|
||||
|
||||
output: "export",
|
||||
basePath: `/ProxmoxVE`,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
10090
frontend/package-lock.json
generated
10090
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"name": "proxmox-helper-scripts-website",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Bram Suurd",
|
||||
"url": "https://github.com/community-scripts"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.11.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"next": "15.1.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"nuqs": "^2.1.1",
|
||||
"pocketbase": "^0.21.4",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"react": "19.0.0-rc-02c0e824-20241028",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"simple-icons": "^13.5.0",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@types/node": "^22",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"eslint": "^9.13.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-animated": "^1.1.2",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5",
|
||||
"vite-tsconfig-paths": "^5.1.3",
|
||||
"vitest": "^2.1.6"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
@@ -1 +0,0 @@
|
||||
../../json
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -1,11 +0,0 @@
|
||||
import { screen } from "@testing-library/dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Page from "@/app/page";
|
||||
|
||||
describe("Page", () => {
|
||||
it("should show button to view scripts", () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByRole("button", { name: "View Scripts" })).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, it, assert, beforeAll } from "vitest";
|
||||
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";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const fileNames = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName !== metadataFileName)
|
||||
|
||||
describe.each(fileNames)("%s", async (fileName) => {
|
||||
let script: Script;
|
||||
|
||||
beforeAll(async () => {
|
||||
const filePath = path.resolve(jsonDir, fileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding)
|
||||
script = JSON.parse(fileContent);
|
||||
})
|
||||
|
||||
it("should have valid json according to script schema", () => {
|
||||
ScriptSchema.parse(script);
|
||||
});
|
||||
|
||||
it("should have a corresponding script file", () => {
|
||||
script.install_methods.forEach((method) => {
|
||||
const scriptPath = path.resolve("..", method.script)
|
||||
assert(fs.stat(scriptPath), `Script file not found: ${scriptPath}`)
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
describe(`${metadataFileName}`, async () => {
|
||||
let metadata: Metadata;
|
||||
|
||||
beforeAll(async () => {
|
||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||
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);
|
||||
metadata.categories.forEach((category) => {
|
||||
assert.isString(category.name)
|
||||
assert.isNumber(category.id)
|
||||
assert.isNumber(category.sort_order)
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock canvas getContext
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn();
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Metadata, Script } from "@/lib/types";
|
||||
import { promises as fs } from "fs";
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getMetadata = async () => {
|
||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const metadata: Metadata = JSON.parse(fileContent);
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const getScripts = async () => {
|
||||
const filePaths = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName !== metadataFileName)
|
||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
||||
|
||||
const scripts = await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const script: Script = JSON.parse(fileContent);
|
||||
return script;
|
||||
}),
|
||||
);
|
||||
return scripts;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const metadata = await getMetadata();
|
||||
const scripts = await getScripts();
|
||||
|
||||
const categories = metadata.categories
|
||||
.map((category) => {
|
||||
category.scripts = scripts.filter((script) =>
|
||||
script.categories.includes(category.id),
|
||||
);
|
||||
return category;
|
||||
})
|
||||
.sort((a, b) => a.sort_order - b.sort_order);
|
||||
|
||||
return NextResponse.json(categories);
|
||||
} catch (error) {
|
||||
console.error(error as Error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch categories" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,117 +0,0 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { type Script } from "../_schemas/schemas";
|
||||
import { memo } from "react";
|
||||
|
||||
type CategoryProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
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,
|
||||
setScript,
|
||||
categories,
|
||||
}: Omit<CategoryProps, "setIsValid" | "setZodErrors">) {
|
||||
const addCategory = (categoryId: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
categories: [...new Set([...script.categories, categoryId])],
|
||||
});
|
||||
};
|
||||
|
||||
const removeCategory = (categoryId: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
categories: script.categories.filter((id: number) => id !== categoryId),
|
||||
});
|
||||
};
|
||||
|
||||
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2",
|
||||
script.categories.length !== 0 && "mt-2",
|
||||
)}
|
||||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Categories);
|
||||
@@ -1,240 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { OperatingSystems } from "@/config/siteConfig";
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
|
||||
type InstallMethodProps = {
|
||||
script: Script;
|
||||
setScript: (value: Script | ((prevState: Script) => Script)) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function InstallMethod({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: InstallMethodProps) {
|
||||
const cpuRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const ramRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const hddRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addInstallMethod = useCallback(() => {
|
||||
setScript((prev) => {
|
||||
const method = InstallMethodSchema.parse({
|
||||
type: "default",
|
||||
script: `${prev.type}/${prev.slug}.sh`,
|
||||
resources: {
|
||||
cpu: null,
|
||||
ram: null,
|
||||
hdd: null,
|
||||
os: null,
|
||||
version: null,
|
||||
},
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
install_methods: [...prev.install_methods, method],
|
||||
};
|
||||
});
|
||||
}, [setScript]);
|
||||
|
||||
const updateInstallMethod = useCallback(
|
||||
(
|
||||
index: number,
|
||||
key: keyof Script["install_methods"][number],
|
||||
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
||||
) => {
|
||||
setScript((prev) => {
|
||||
const updatedMethods = prev.install_methods.map((method, i) => {
|
||||
if (i === index) {
|
||||
const updatedMethod = { ...method, [key]: value };
|
||||
|
||||
if (key === "type") {
|
||||
updatedMethod.script =
|
||||
value === "alpine"
|
||||
? `/${prev.type}/alpine-${prev.slug}.sh`
|
||||
: `/${prev.type}/${prev.slug}.sh`;
|
||||
|
||||
// Set OS to Alpine and reset version if type is alpine
|
||||
if (value === "alpine") {
|
||||
updatedMethod.resources.os = "Alpine";
|
||||
updatedMethod.resources.version = null;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMethod;
|
||||
}
|
||||
return method;
|
||||
});
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
install_methods: updatedMethods,
|
||||
};
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[setScript, setIsValid, setZodErrors],
|
||||
);
|
||||
|
||||
const removeInstallMethod = useCallback(
|
||||
(index: number) => {
|
||||
setScript((prev) => ({
|
||||
...prev,
|
||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||
}));
|
||||
},
|
||||
[setScript],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Install Methods</h3>
|
||||
{script.install_methods.map((method, index) => (
|
||||
<div key={index} className="space-y-2 border p-4 rounded">
|
||||
<Select
|
||||
value={method.type}
|
||||
onValueChange={(value) => updateInstallMethod(index, "type", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="alpine">Alpine</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={(el) => {
|
||||
cpuRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
ramRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
hddRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={method.resources.os || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: value || null,
|
||||
version: null, // Reset version when OS changes
|
||||
})
|
||||
}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.map((os) => (
|
||||
<SelectItem key={os.name} value={os.name}>
|
||||
{os.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={method.resources.version || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: value || null,
|
||||
})
|
||||
}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.find(
|
||||
(os) => os.name === method.resources.os,
|
||||
)?.versions.map((version) => (
|
||||
<SelectItem key={version.slug} value={version.name}>
|
||||
{version.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => removeInstallMethod(index)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={script.install_methods.length >= 2}
|
||||
onClick={addInstallMethod}
|
||||
>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(InstallMethod);
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
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;
|
||||
};
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
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 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>
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Note);
|
||||
@@ -1,45 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const InstallMethodSchema = z.object({
|
||||
type: z.enum(["default", "alpine"], {
|
||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" })
|
||||
}),
|
||||
script: z.string().min(1, "Script content cannot be empty"),
|
||||
resources: z.object({
|
||||
cpu: z.number().nullable(),
|
||||
ram: z.number().nullable(),
|
||||
hdd: z.number().nullable(),
|
||||
os: z.string().nullable(),
|
||||
version: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
const NoteSchema = z.object({
|
||||
text: z.string().min(1, "Note text cannot be empty"),
|
||||
type: z.string().min(1, "Note type cannot be empty"),
|
||||
});
|
||||
|
||||
export const ScriptSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
slug: z.string().min(1, "Slug is required"),
|
||||
categories: z.array(z.number()),
|
||||
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
||||
type: z.enum(["vm", "ct", "misc", "turnkey"], {
|
||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'misc' or 'turnkey'" })
|
||||
}),
|
||||
updateable: z.boolean(),
|
||||
privileged: z.boolean(),
|
||||
interface_port: z.number().nullable(),
|
||||
documentation: z.string().nullable(),
|
||||
website: z.string().url().nullable(),
|
||||
logo: z.string().url().nullable(),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
|
||||
default_credentials: z.object({
|
||||
username: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
}),
|
||||
notes: z.array(NoteSchema),
|
||||
});
|
||||
|
||||
export type Script = z.infer<typeof ScriptSchema>;
|
||||
@@ -1,335 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon, Check, Clipboard } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
import InstallMethod from "./_components/InstallMethod";
|
||||
import Note from "./_components/Note";
|
||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
||||
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
date_created: "",
|
||||
type: "ct",
|
||||
updateable: false,
|
||||
privileged: false,
|
||||
interface_port: null,
|
||||
documentation: null,
|
||||
website: null,
|
||||
logo: null,
|
||||
description: "",
|
||||
install_methods: [],
|
||||
default_credentials: {
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
notes: [],
|
||||
};
|
||||
|
||||
export default function JSONGenerator() {
|
||||
const [script, setScript] = useState<Script>(initialScript);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then(setCategories)
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
const updateScript = useCallback(
|
||||
(key: keyof Script, value: Script[keyof Script]) => {
|
||||
setScript((prev) => {
|
||||
const updated = { ...prev, [key]: value };
|
||||
|
||||
if (key === "type" || key === "slug") {
|
||||
updated.install_methods = updated.install_methods.map((method) => ({
|
||||
...method,
|
||||
script:
|
||||
method.type === "alpine"
|
||||
? `/${updated.type}/alpine-${updated.slug}.sh`
|
||||
: `/${updated.type}/${updated.slug}.sh`,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
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 (
|
||||
<div className="flex h-screen mt-20">
|
||||
<div className="w-1/2 p-4 overflow-y-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">JSON Generator</h2>
|
||||
<form className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Example"
|
||||
value={script.name}
|
||||
onChange={(e) => updateScript("name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="example"
|
||||
value={script.slug}
|
||||
onChange={(e) => updateScript("slug", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Logo <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
categories={categories}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Date Created</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"pl-3 text-left font-normal w-full",
|
||||
!script.date_created && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(script.date_created)}
|
||||
onSelect={handleDateSelect}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select
|
||||
value={script.type}
|
||||
onValueChange={(value) => updateScript("type", value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ct">LXC Container</SelectItem>
|
||||
<SelectItem value="vm">Virtual Machine</SelectItem>
|
||||
<SelectItem value="misc">Miscellaneous</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex gap-5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={script.updateable}
|
||||
onCheckedChange={(checked) =>
|
||||
updateScript("updateable", checked)
|
||||
}
|
||||
/>
|
||||
<label>Updateable</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={script.privileged}
|
||||
onCheckedChange={(checked) =>
|
||||
updateScript("privileged", checked)
|
||||
}
|
||||
/>
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={(e) =>
|
||||
updateScript(
|
||||
"interface_port",
|
||||
e.target.value ? Number(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Website URL"
|
||||
value={script.website || ""}
|
||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("documentation", e.target.value || null)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
setIsValid={setIsValid}
|
||||
setZodErrors={setZodErrors}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold">Default Credentials</h3>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Note
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
setIsValid={setIsValid}
|
||||
setZodErrors={setZodErrors}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||
{validationAlert}
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="absolute right-2 top-2"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Clipboard className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<pre className="mt-4 p-4 bg-secondary rounded shadow overflow-x-scroll">
|
||||
{JSON.stringify(script, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { analytics, basePath } from "@/config/siteConfig";
|
||||
import "@/styles/globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import React from "react";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: "Proxmox VE Helper-Scripts",
|
||||
generator: "Next.js",
|
||||
applicationName: "Proxmox VE Helper-Scripts",
|
||||
referrer: "origin-when-cross-origin",
|
||||
keywords: [
|
||||
"Proxmox VE",
|
||||
"Helper-Scripts",
|
||||
"tteck",
|
||||
"helper",
|
||||
"scripts",
|
||||
"proxmox",
|
||||
"VE",
|
||||
],
|
||||
authors: { name: "Bram Suurd" },
|
||||
creator: "Bram Suurd",
|
||||
publisher: "Bram Suurd",
|
||||
description:
|
||||
"A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
favicon: "/app/favicon.ico",
|
||||
formatDetection: {
|
||||
email: false,
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL(`https://community-scripts.github.io/${basePath}/`),
|
||||
openGraph: {
|
||||
title: "Proxmox VE Helper-Scripts",
|
||||
description:
|
||||
"A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
url: "/defaultimg.png",
|
||||
images: [
|
||||
{
|
||||
url: `https://community-scripts.github.io/${basePath}/defaultimg.png`,
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
defer
|
||||
src={`https://${analytics.url}/script.js`}
|
||||
data-website-id={analytics.token}
|
||||
></script>
|
||||
<link rel="canonical" href={metadata.metadataBase.href} />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex w-full flex-col justify-center">
|
||||
<Navbar />
|
||||
<div className="flex min-h-screen flex-col justify-center">
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-full max-w-7xl ">
|
||||
<NuqsAdapter>{children}</NuqsAdapter>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Proxmox VE Helper-Scripts",
|
||||
short_name: "Proxmox VE Helper-Scripts",
|
||||
description:
|
||||
"A Re-designed Front-end for the Proxmox VE Helper-Scripts Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
theme_color: "#030712",
|
||||
background_color: "#030712",
|
||||
display: "standalone",
|
||||
orientation: "portrait",
|
||||
scope: `${basePath}`,
|
||||
start_url: `${basePath}`,
|
||||
icons: [
|
||||
{
|
||||
src: "logo.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
404
|
||||
</h1>
|
||||
<p className="text-muted-foreground md:text-xl">
|
||||
Oops, the page you are looking for could not be found.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => window.history.back()} variant="secondary">
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
"use client";
|
||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import Particles from "@/components/ui/particles";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
|
||||
function CustomArrowRightIcon() {
|
||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const [color, setColor] = useState("#000000");
|
||||
|
||||
useEffect(() => {
|
||||
setColor(theme === "dark" ? "#ffffff" : "#000000");
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="w-full mt-16">
|
||||
<Particles
|
||||
className="absolute inset-0 -z-40"
|
||||
quantity={100}
|
||||
ease={80}
|
||||
color={color}
|
||||
refresh
|
||||
/>
|
||||
<div className="container mx-auto">
|
||||
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<div>
|
||||
<AnimatedGradientText>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`inline`,
|
||||
)}
|
||||
>
|
||||
Scripts by Tteck
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thank You!</DialogTitle>
|
||||
<DialogDescription>
|
||||
A big thank you to Tteck and the many contributors who have
|
||||
made this project possible. Your hard work is truly
|
||||
appreciated by the entire Proxmox community!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://github.com/tteck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper
|
||||
Scripts
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-5xl font-semibold tracking-tighter md:text-7xl">
|
||||
Make managing your Homelab a breeze
|
||||
</h1>
|
||||
<p className="max-w-2xl text-center text-lg leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
We are a community-driven initiative that simplifies the setup of
|
||||
Proxmox Virtual Environment (VE).
|
||||
<br />
|
||||
<br />
|
||||
Originally created by{" "}
|
||||
<a href="https://github.com/tteck" target="_blank">
|
||||
tteck
|
||||
</a>
|
||||
, these scripts automate and streamline
|
||||
<br />
|
||||
the process of creating and configuring Linux containers (LXC) and
|
||||
virtual machines (VMs) on Proxmox VE.
|
||||
<br />
|
||||
<br />
|
||||
With 200+ scripts to help you manage your{" "}
|
||||
<b>Proxmox VE environment</b>.<br />
|
||||
Whether you're a seasoned user or a newcomer, we've got
|
||||
you covered.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/scripts">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="expandIcon"
|
||||
Icon={CustomArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="hover:"
|
||||
>
|
||||
View Scripts
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: `https://community-scripts.github.io/${basePath}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { formattedBadge } from "@/components/CommandMenu";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
export default function ScriptAccordion({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) {
|
||||
const [expandedItem, setExpandedItem] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
|
||||
|
||||
const handleAccordionChange = (value: string | undefined) => {
|
||||
setExpandedItem(value);
|
||||
};
|
||||
|
||||
const handleSelected = useCallback(
|
||||
(slug: string) => {
|
||||
setSelectedScript(slug);
|
||||
},
|
||||
[setSelectedScript],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
const category = items.find((category) =>
|
||||
category.scripts.some((script) => script.slug === selectedScript),
|
||||
);
|
||||
if (category) {
|
||||
setExpandedItem(category.name);
|
||||
handleSelected(selectedScript);
|
||||
}
|
||||
}
|
||||
}, [selectedScript, items, handleSelected]);
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
value={expandedItem}
|
||||
onValueChange={handleAccordionChange}
|
||||
collapsible
|
||||
className="overflow-y-scroll max-h-[calc(100vh-220px)] overflow-x-hidden mt-3 p-2"
|
||||
>
|
||||
{items.map((category) => (
|
||||
<AccordionItem
|
||||
key={category.id + ":category"}
|
||||
value={category.name}
|
||||
className={cn("sm:text-md flex flex-col border-none", {
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
<div className="mr-2 flex w-full items-center justify-between">
|
||||
<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.scripts.length}
|
||||
</span>
|
||||
</div>{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
data-state={expandedItem === category.name ? "open" : "closed"}
|
||||
className="pt-0"
|
||||
>
|
||||
{category.scripts
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script, index) => (
|
||||
<div key={index}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
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.slug
|
||||
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleSelected(script.slug)}
|
||||
ref={(el) => {
|
||||
linkRefs.current[script.slug] = el;
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
height={16}
|
||||
width={16}
|
||||
unoptimized
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/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>
|
||||
{formattedBadge(script.type)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { basePath, mostPopularScripts } from "@/config/siteConfig";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { CalendarPlus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const ITEMS_PER_PAGE = 3;
|
||||
|
||||
export const getDisplayValueFromType = (type: string) => {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "misc":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const latestScripts = useMemo(() => {
|
||||
if (!items) return [];
|
||||
const scripts = items.flatMap((category) => category.scripts || []);
|
||||
return scripts.sort(
|
||||
(a, b) =>
|
||||
new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const goToNextPage = () => {
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
};
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
setPage((prevPage) => prevPage - 1);
|
||||
};
|
||||
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = page * ITEMS_PER_PAGE;
|
||||
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{latestScripts.length > 0 && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Newest Scripts</h2>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{page > 1 && (
|
||||
<div
|
||||
className="cursor-pointer select-none p-2 text-sm font-semibold"
|
||||
onClick={goToPreviousPage}
|
||||
>
|
||||
Previous
|
||||
</div>
|
||||
)}
|
||||
{endIndex < latestScripts.length && (
|
||||
<div
|
||||
onClick={goToNextPage}
|
||||
className="cursor-pointer select-none p-2 text-sm font-semibold"
|
||||
>
|
||||
{page === 1 ? "More.." : "Next"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{latestScripts.slice(startIndex, endIndex).map((script) => (
|
||||
<Card
|
||||
key={script.slug}
|
||||
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={script.logo || `/${basePath}/logo.png`}
|
||||
unoptimized
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg line-clamp-1">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground">
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) =>
|
||||
mostPopularScripts.includes(script.name),
|
||||
);
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
|
||||
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.map((script) => (
|
||||
<Card
|
||||
key={script.slug}
|
||||
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={script.logo || `/${basePath}/logo.png`}
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="line-clamp-1 text-lg">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground break-words">
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
prefetch={false}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
"use client";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Script } from "@/lib/types";
|
||||
import { X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
||||
import Alerts from "./ScriptItems/Alerts";
|
||||
import Buttons from "./ScriptItems/Buttons";
|
||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
||||
import DefaultSettings from "./ScriptItems/DefaultSettings";
|
||||
import Description from "./ScriptItems/Description";
|
||||
import InstallCommand from "./ScriptItems/InstallCommand";
|
||||
import InterFaces from "./ScriptItems/InterFaces";
|
||||
import Tooltips from "./ScriptItems/Tooltips";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
function ScriptItem({
|
||||
item,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
item: Script;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) {
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-7 mt-0 flex w-full min-w-fit">
|
||||
<div className="flex w-full min-w-fit">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex h-[36px] min-w-max items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Selected Script</h2>
|
||||
<X onClick={closeScript} className="cursor-pointer" />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-accent/20 p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
|
||||
src={item.logo || `/${basePath}/logo.png`}
|
||||
width={400}
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
height={400}
|
||||
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.name} {getDisplayValueFromType(item.type)}
|
||||
</h1>
|
||||
<p className="w-full text-sm text-muted-foreground">
|
||||
Date added: {extractDate(item.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<DefaultSettings item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden flex-col justify-between gap-2 sm:flex">
|
||||
<InterFaces item={item} />
|
||||
<Buttons item={item} />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mt-4" />
|
||||
<div>
|
||||
<div className="mt-4">
|
||||
<Description item={item} />
|
||||
<Alerts item={item} />
|
||||
</div>
|
||||
<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.type == "misc" ? "use" : "install"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</div>
|
||||
<Separator className="w-full"></Separator>
|
||||
<InstallCommand item={item} />
|
||||
</div>
|
||||
</div>
|
||||
<DefaultPassword item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScriptItem;
|
||||
@@ -1,35 +0,0 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertCircle, NotepadText } from "lucide-react";
|
||||
|
||||
type NoteProps = {
|
||||
text: string;
|
||||
type: keyof typeof AlertColors;
|
||||
}
|
||||
|
||||
export default function Alerts({ item }: { item: Script }) {
|
||||
return (
|
||||
<>
|
||||
{item?.notes?.length > 0 &&
|
||||
item.notes.map((note: NoteProps, index: number) => (
|
||||
<div key={index} className="mt-4 flex flex-col gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm",
|
||||
AlertColors[note.type],
|
||||
)}
|
||||
>
|
||||
{note.type == "info" ? (
|
||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||
)}
|
||||
<span>{TextCopyBlock(note.text)}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { BookOpenText, Code, Globe } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
const generateSourceUrl = (slug: string, type: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return type === "ct"
|
||||
? `${baseUrl}/install/${slug}-install.sh`
|
||||
: `${baseUrl}/${type}/${slug}.sh`;
|
||||
};
|
||||
|
||||
interface ButtonLinkProps {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const ButtonLink = ({ href, icon, text }: ButtonLinkProps) => (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link target="_blank" href={href}>
|
||||
<span className="flex items-center gap-2">
|
||||
{icon}
|
||||
{text}
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default function Buttons({ item }: { item: Script }) {
|
||||
const buttons = [
|
||||
item.website && {
|
||||
href: item.website,
|
||||
icon: <Globe className="h-4 w-4" />,
|
||||
text: "Website",
|
||||
},
|
||||
item.documentation && {
|
||||
href: item.documentation,
|
||||
icon: <BookOpenText className="h-4 w-4" />,
|
||||
text: "Documentation",
|
||||
},
|
||||
{
|
||||
href: generateSourceUrl(item.slug, item.type),
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Source Code",
|
||||
},
|
||||
].filter(Boolean) as ButtonLinkProps[];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{buttons.map((props, index) => (
|
||||
<ButtonLink key={index} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import handleCopy from "@/components/handleCopy";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultPassword({ item }: { item: Script }) {
|
||||
const { username, password } = item.default_credentials;
|
||||
const hasDefaultLogin = username && password;
|
||||
|
||||
if (!hasDefaultLogin) return null;
|
||||
|
||||
const copyCredential = (type: "username" | "password") => {
|
||||
handleCopy(type, item.default_credentials[type] ?? "");
|
||||
};
|
||||
|
||||
return (
|
||||
<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">Default Login Credentials</h2>
|
||||
</div>
|
||||
<Separator className="w-full" />
|
||||
<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.name}{" "}
|
||||
{item.type}.
|
||||
</p>
|
||||
{["username", "password"].map((type) => (
|
||||
<div key={type} className="text-sm">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="null"
|
||||
onClick={() => copyCredential(type as "username" | "password")}
|
||||
>
|
||||
{item.default_credentials[type as "username" | "password"]}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultSettings({ item }: { item: Script }) {
|
||||
const getDisplayValueFromRAM = (ram: number) =>
|
||||
ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`;
|
||||
|
||||
const ResourceDisplay = ({
|
||||
settings,
|
||||
title,
|
||||
}: {
|
||||
settings: (typeof item.install_methods)[0];
|
||||
title: string;
|
||||
}) => {
|
||||
const { cpu, ram, hdd } = settings.resources;
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-md font-semibold">{title}</h2>
|
||||
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RAM: {getDisplayValueFromRAM(ram ?? 0)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultSettings = item.install_methods.find(
|
||||
(method) => method.type === "default",
|
||||
);
|
||||
const defaultAlpineSettings = item.install_methods.find(
|
||||
(method) => method.type === "alpine",
|
||||
);
|
||||
|
||||
const hasDefaultSettings =
|
||||
defaultSettings?.resources &&
|
||||
Object.values(defaultSettings.resources).some(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDefaultSettings && (
|
||||
<ResourceDisplay settings={defaultSettings} title="Default settings" />
|
||||
)}
|
||||
{defaultAlpineSettings && (
|
||||
<ResourceDisplay
|
||||
settings={defaultAlpineSettings}
|
||||
title="Default Alpine settings"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function Description({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{TextCopyBlock(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
||||
|
||||
const getInstallCommand = (scriptPath?: string, isAlpine = false) => {
|
||||
return `bash -c "$(wget -q${isAlpine ? "" : "L"}O - https://github.com/community-scripts/${basePath}/raw/main/${scriptPath})"`;
|
||||
};
|
||||
|
||||
export default function InstallCommand({ item }: { item: Script }) {
|
||||
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{" "}
|
||||
{item.name} package to create a {item.name}{" "}
|
||||
{getDisplayValueFromType(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 == "misc" ? (
|
||||
<>
|
||||
To use the {item.name} script, run the command below in the shell.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
To create a new Proxmox VE {item.name}{" "}
|
||||
{getDisplayValueFromType(item.type)}, run the command below in the
|
||||
Proxmox VE Shell.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{isAlpine && (
|
||||
<p className="mt-2 text-sm">
|
||||
To create a new Proxmox VE Alpine-{item.name}{" "}
|
||||
{getDisplayValueFromType(item.type)}, run the command below in the
|
||||
Proxmox VE Shell
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{alpineScript ? (
|
||||
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
|
||||
<TabsList>
|
||||
<TabsTrigger value="default">Default</TabsTrigger>
|
||||
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="default">
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>
|
||||
{getInstallCommand(defaultScript?.script)}
|
||||
</CodeCopyButton>
|
||||
</TabsContent>
|
||||
<TabsContent value="alpine">
|
||||
{renderInstructions(true)}
|
||||
<CodeCopyButton>
|
||||
{getInstallCommand(alpineScript.script, true)}
|
||||
</CodeCopyButton>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : defaultScript?.script ? (
|
||||
<>
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>
|
||||
{getInstallCommand(defaultScript.script)}
|
||||
</CodeCopyButton>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import handleCopy from "@/components/handleCopy";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ClipboardIcon } from "lucide-react";
|
||||
|
||||
const CopyButton = ({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}) => (
|
||||
<span
|
||||
className={cn(
|
||||
buttonVariants({ size: "sm", variant: "secondary" }),
|
||||
"flex items-center gap-2",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
<ClipboardIcon
|
||||
onClick={() => handleCopy(label, String(value))}
|
||||
className="size-4 cursor-pointer"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
export default function InterFaces({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{item.interface_port !== null ? (
|
||||
<div className="flex items-center justify-end">
|
||||
<h2 className="mr-2 text-end text-lg font-semibold">
|
||||
{"Default Interface:"}
|
||||
</h2>{" "}
|
||||
<CopyButton label="default interface" value={item.interface_port} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Script } from "@/lib/types";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
variant: "warning" | "success";
|
||||
label: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className="flex items-center">
|
||||
<Badge variant={variant} className="flex items-center gap-1">
|
||||
{label} <CircleHelp className="size-3" />
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-sm max-w-64">
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
export default function Tooltips({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.privileged && (
|
||||
<TooltipBadge
|
||||
variant="warning"
|
||||
label="Privileged"
|
||||
content="This script will be run in a privileged LXC"
|
||||
/>
|
||||
)}
|
||||
{item.updateable && (
|
||||
<TooltipBadge
|
||||
variant="success"
|
||||
label="Updateable"
|
||||
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Category } from "@/lib/types";
|
||||
import ScriptAccordion from "./ScriptAccordion";
|
||||
|
||||
const Sidebar = ({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex min-w-72 flex-col sm:max-w-72">
|
||||
<div className="flex items-end justify-between pb-4">
|
||||
<h1 className="text-xl font-bold">Categories</h1>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
{items.reduce((acc, category) => acc + category.scripts.length, 0)}{" "}
|
||||
Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={items}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,79 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
import ScriptItem from "@/app/scripts/_components/ScriptItem";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import {
|
||||
LatestScripts,
|
||||
MostViewedScripts,
|
||||
} from "./_components/ScriptInfoBlocks";
|
||||
import Sidebar from "./_components/Sidebar";
|
||||
|
||||
function ScriptContent() {
|
||||
const [selectedScript, setSelectedScript] = useQueryState("id");
|
||||
const [links, setLinks] = useState<Category[]>([]);
|
||||
const [item, setItem] = useState<Script>();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedScript && links.length > 0) {
|
||||
const script = links
|
||||
.map((category) => category.scripts)
|
||||
.flat()
|
||||
.find((script) => script.slug === selectedScript);
|
||||
setItem(script);
|
||||
}
|
||||
}, [selectedScript, links]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 flex sm:px-4 xl:px-0">
|
||||
<div className="hidden sm:flex">
|
||||
<Sidebar
|
||||
items={links}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-7 w-full sm:mx-0 sm:ml-7">
|
||||
{selectedScript && item ? (
|
||||
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={links} />
|
||||
<MostViewedScripts items={links} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ScriptContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
let domain = "community-scripts.github.io";
|
||||
let protocol = "https";
|
||||
return [
|
||||
{
|
||||
url: `${protocol}://${domain}/${basePath}`,
|
||||
lastModified: new Date(),
|
||||
},
|
||||
{
|
||||
url: `${protocol}://${domain}/${basePath}/scripts`,
|
||||
lastModified: new Date(),
|
||||
},
|
||||
{
|
||||
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
||||
lastModified: new Date(),
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
export const formattedBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
||||
case "ct":
|
||||
return (
|
||||
<Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>
|
||||
);
|
||||
case "misc":
|
||||
return <Badge className="text-green-500/75 border-green-500/75">MISC</Badge>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function CommandMenu() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [links, setLinks] = React.useState<Category[]>([]);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
fetchSortedCategories();
|
||||
setOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
const fetchSortedCategories = () => {
|
||||
setIsLoading(true);
|
||||
fetchCategories()
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"relative h-9 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
|
||||
)}
|
||||
onClick={() => {
|
||||
fetchSortedCategories();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="inline-flex">Search scripts...</span>
|
||||
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span className="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||
<CommandInput placeholder="Search for a script..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? "Loading..." : "No scripts found."}
|
||||
</CommandEmpty>
|
||||
{links.map((category) => (
|
||||
<CommandGroup
|
||||
key={`category:${category.name}`}
|
||||
heading={category.name}
|
||||
>
|
||||
{category.scripts.map((script) => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.slug}-${script.name}`}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto flex border-t border-border bg-background/40 py-6 backdrop-blur-lg">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mx-6 w-full max-w-7xl text-sm text-muted-foreground">
|
||||
Website build by the community. The source code is avaliable on{" "}
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-semibold underline-offset-2 duration-300 hover:underline"
|
||||
data-umami-event="View Website Source Code on Github"
|
||||
>
|
||||
GitHub
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { navbarLinks } from "@/config/siteConfig";
|
||||
|
||||
import CommandMenu from "./CommandMenu";
|
||||
import StarOnGithubButton from "./ui/star-on-github-button";
|
||||
import { ThemeToggle } from "./ui/theme-toggle";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function Navbar() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 0);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
|
||||
isScrolled ? "glass border-b bg-background/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-7xl flex-row-reverse items-center justify-between sm:flex-row">
|
||||
<Link
|
||||
href={"/"}
|
||||
className="flex cursor-pointer flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
|
||||
>
|
||||
<Image
|
||||
height={18}
|
||||
unoptimized
|
||||
width={18}
|
||||
alt="logo"
|
||||
src="logo.png"
|
||||
/>
|
||||
<span className="hidden lg:block">Proxmox VE Helper-Scripts</span>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<CommandMenu />
|
||||
<StarOnGithubButton />
|
||||
{navbarLinks.map(({ href, event, icon, text }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size={"icon"} asChild>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={href}
|
||||
data-umami-event={event}
|
||||
>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ClipboardIcon } from "lucide-react";
|
||||
import handleCopy from "./handleCopy";
|
||||
|
||||
export default function TextCopyBlock(description: string) {
|
||||
const pattern = /`([^`]*)`/g;
|
||||
const parts = description.split(pattern);
|
||||
|
||||
const formattedDescription = parts.map((part: string, index: number) => {
|
||||
if (index % 2 === 1) {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-secondary py-1 px-2 rounded-lg inline-flex items-center gap-2"
|
||||
>
|
||||
{part}
|
||||
<ClipboardIcon
|
||||
className="size-3 cursor-pointer"
|
||||
onClick={() => handleCopy("command", part)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return part;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedDescription;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ClipboardCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function handleCopy(type: string, value: string) {
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
toast.success(`copied ${type} to clipboard`, {
|
||||
icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-1 pr-2 font-medium transition-all [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden py-1 text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AnimatedGradientText({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-1.5 py-0.1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent text-primary-foreground border-primary-foreground",
|
||||
secondary:
|
||||
"border-transparent text-secondary-foreground border-secondary-foreground",
|
||||
destructive:
|
||||
"border-transparent text-destructive-foreground border-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
success: "text-green-500 border-green-500",
|
||||
warning: "text-yellow-500 border-yellow-500",
|
||||
failure: "text-red-500 border-red-500",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,108 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
expandIcon:
|
||||
"group relative text-primary-foreground bg-primary hover:bg-primary/90",
|
||||
ringHover:
|
||||
"bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
|
||||
shine:
|
||||
"text-primary-foreground animate-shine bg-gradient-to-r from-primary via-primary/75 to-primary bg-[length:400%_100%] ",
|
||||
gooeyRight:
|
||||
"text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-zinc-400 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%] ",
|
||||
gooeyLeft:
|
||||
"text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-zinc-400 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%] ",
|
||||
linkHover1:
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
|
||||
linkHover2:
|
||||
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-9 w-9 ",
|
||||
null: "py-1 px-3 rouded-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IconProps {
|
||||
Icon: React.ElementType;
|
||||
iconPlacement: "left" | "right";
|
||||
}
|
||||
|
||||
interface IconRefProps {
|
||||
Icon?: never;
|
||||
iconPlacement?: undefined;
|
||||
}
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export type ButtonIconProps = IconProps | IconRefProps;
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & ButtonIconProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
Icon,
|
||||
iconPlacement,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{Icon && iconPlacement === "left" && (
|
||||
<div className="group-hover:translate-x-100 w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:pr-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
<Slottable>{props.children}</Slottable>
|
||||
{Icon && iconPlacement === "right" && (
|
||||
<div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
||||
@@ -1,89 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"min-h-[40px] text-sm text-muted-foreground sm:min-h-[60px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("mt-auto items-center p-4 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Card } from "./card";
|
||||
|
||||
export default function CodeCopyButton({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const isMobile = window.innerWidth <= 640;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCopied) {
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
}, [hasCopied]);
|
||||
|
||||
const handleCopy = (type: string, value: any) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
setHasCopied(true);
|
||||
|
||||
let warning = localStorage.getItem("warning");
|
||||
|
||||
if (warning === null) {
|
||||
localStorage.setItem("warning", "1");
|
||||
setTimeout(() => {
|
||||
toast.error(
|
||||
"Be careful when copying scripts from the internet. Always remember check the source!",
|
||||
{ duration: 8000 },
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// toast.success(`copied ${type} to clipboard`, {
|
||||
// icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex">
|
||||
<Card className="flex items-center overflow-x-auto bg-primary-foreground pl-4">
|
||||
<div className="overflow-x-auto whitespace-pre-wrap text-nowrap break-all pr-4 text-sm">
|
||||
{!isMobile && children ? children : "Copy install command"}
|
||||
</div>
|
||||
<div
|
||||
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
||||
onClick={() => handleCopy("install command", children)}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Clipboard, Copy } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "./button";
|
||||
import { Separator } from "./separator";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:border-primary hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary border-secondary text-secondary-foreground hover:bg-secondary/80 hover:border-primary",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
null: "py-1 px-3 rouded-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleCopy = (type: string, value: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
||||
|
||||
if (amountOfScriptsCopied === null) {
|
||||
localStorage.setItem("amountOfScriptsCopied", "1");
|
||||
} else {
|
||||
amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString();
|
||||
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
||||
|
||||
if (
|
||||
parseInt(amountOfScriptsCopied) === 3 ||
|
||||
parseInt(amountOfScriptsCopied) === 10 ||
|
||||
parseInt(amountOfScriptsCopied) === 25 ||
|
||||
parseInt(amountOfScriptsCopied) === 50 ||
|
||||
parseInt(amountOfScriptsCopied) === 100
|
||||
) {
|
||||
setTimeout(() => {
|
||||
toast.info(
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="lg">
|
||||
If you find these scripts useful, please consider starring the
|
||||
repository on GitHub. It helps a lot!
|
||||
</p>
|
||||
<div>
|
||||
<Button className="text-white">
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
data-umami-event="Star on Github"
|
||||
target="_blank"
|
||||
>
|
||||
Star on GitHub 💫
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
{ duration: 8000 },
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
<div className="flex items-center gap-2">
|
||||
<Clipboard className="h-4 w-4" />
|
||||
<span>Copied {type} to clipboard</span>
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
|
||||
export interface CodeBlockProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
({ className, variant, size, asChild = false, code }, ref) => {
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(code);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
marginBottom: "1rem",
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<pre
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
" flex flex-row p-4",
|
||||
)}
|
||||
>
|
||||
<p className="flex items-center gap-2">
|
||||
{code} <Separator orientation="vertical" />{" "}
|
||||
<Copy
|
||||
className="cursor-pointer"
|
||||
size={16}
|
||||
onClick={() => handleCopy("install command", code)}
|
||||
/>
|
||||
</p>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
|
||||
export { buttonVariants, CodeBlock };
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-51%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"glass z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover/50 p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -1,128 +0,0 @@
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useInView, useMotionValue, useSpring } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function NumberTicker({
|
||||
value,
|
||||
direction = "up",
|
||||
delay = 0,
|
||||
className,
|
||||
decimalPlaces = 0,
|
||||
}: {
|
||||
value: number;
|
||||
direction?: "up" | "down";
|
||||
className?: string;
|
||||
delay?: number; // delay in s
|
||||
decimalPlaces?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const motionValue = useMotionValue(direction === "down" ? value : 0);
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping: 60,
|
||||
stiffness: 100,
|
||||
});
|
||||
const isInView = useInView(ref as React.RefObject<Element>, {
|
||||
once: true,
|
||||
margin: "0px",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
isInView &&
|
||||
setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? 0 : value);
|
||||
}, delay * 1000);
|
||||
}, [motionValue, isInView, delay, value, direction]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
springValue.on("change", (latest) => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(Number(latest.toFixed(decimalPlaces)));
|
||||
}
|
||||
}),
|
||||
[springValue, decimalPlaces],
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block tabular-nums text-black dark:text-white tracking-wider",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function MousePosition(): MousePosition {
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return mousePosition;
|
||||
}
|
||||
|
||||
interface ParticlesProps {
|
||||
className?: string;
|
||||
quantity?: number;
|
||||
staticity?: number;
|
||||
ease?: number;
|
||||
size?: number;
|
||||
refresh?: boolean;
|
||||
color?: string;
|
||||
vx?: number;
|
||||
vy?: number;
|
||||
}
|
||||
function hexToRgb(hex: string): number[] {
|
||||
hex = hex.replace("#", "");
|
||||
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("");
|
||||
}
|
||||
|
||||
const hexInt = parseInt(hex, 16);
|
||||
const red = (hexInt >> 16) & 255;
|
||||
const green = (hexInt >> 8) & 255;
|
||||
const blue = hexInt & 255;
|
||||
return [red, green, blue];
|
||||
}
|
||||
|
||||
const Particles: React.FC<ParticlesProps> = ({
|
||||
className = "",
|
||||
quantity = 100,
|
||||
staticity = 50,
|
||||
ease = 50,
|
||||
size = 0.4,
|
||||
refresh = false,
|
||||
color = "#ffffff",
|
||||
vx = 0,
|
||||
vy = 0,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
const context = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const circles = useRef<Circle[]>([]);
|
||||
const mousePosition = MousePosition();
|
||||
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
context.current = canvasRef.current.getContext("2d");
|
||||
}
|
||||
initCanvas();
|
||||
animate();
|
||||
window.addEventListener("resize", initCanvas);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", initCanvas);
|
||||
};
|
||||
}, [color]);
|
||||
|
||||
useEffect(() => {
|
||||
onMouseMove();
|
||||
}, [mousePosition.x, mousePosition.y]);
|
||||
|
||||
useEffect(() => {
|
||||
initCanvas();
|
||||
}, [refresh]);
|
||||
|
||||
const initCanvas = () => {
|
||||
resizeCanvas();
|
||||
drawParticles();
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (canvasRef.current) {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const { w, h } = canvasSize.current;
|
||||
const x = mousePosition.x - rect.left - w / 2;
|
||||
const y = mousePosition.y - rect.top - h / 2;
|
||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||
if (inside) {
|
||||
mouse.current.x = x;
|
||||
mouse.current.y = y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type Circle = {
|
||||
x: number;
|
||||
y: number;
|
||||
translateX: number;
|
||||
translateY: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
targetAlpha: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
magnetism: number;
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||
circles.current.length = 0;
|
||||
canvasSize.current.w = canvasContainerRef.current.offsetWidth;
|
||||
canvasSize.current.h = canvasContainerRef.current.offsetHeight;
|
||||
canvasRef.current.width = canvasSize.current.w * dpr;
|
||||
canvasRef.current.height = canvasSize.current.h * dpr;
|
||||
canvasRef.current.style.width = `${canvasSize.current.w}px`;
|
||||
canvasRef.current.style.height = `${canvasSize.current.h}px`;
|
||||
context.current.scale(dpr, dpr);
|
||||
}
|
||||
};
|
||||
|
||||
const circleParams = (): Circle => {
|
||||
const x = Math.floor(Math.random() * canvasSize.current.w);
|
||||
const y = Math.floor(Math.random() * canvasSize.current.h);
|
||||
const translateX = 0;
|
||||
const translateY = 0;
|
||||
const pSize = Math.floor(Math.random() * 2) + size;
|
||||
const alpha = 0;
|
||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||
const dx = (Math.random() - 0.5) * 0.1;
|
||||
const dy = (Math.random() - 0.5) * 0.1;
|
||||
const magnetism = 0.1 + Math.random() * 4;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
translateX,
|
||||
translateY,
|
||||
size: pSize,
|
||||
alpha,
|
||||
targetAlpha,
|
||||
dx,
|
||||
dy,
|
||||
magnetism,
|
||||
};
|
||||
};
|
||||
|
||||
const rgb = hexToRgb(color);
|
||||
|
||||
const drawCircle = (circle: Circle, update = false) => {
|
||||
if (context.current) {
|
||||
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||
context.current.translate(translateX, translateY);
|
||||
context.current.beginPath();
|
||||
context.current.arc(x, y, size, 0, 2 * Math.PI);
|
||||
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
|
||||
context.current.fill();
|
||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
if (!update) {
|
||||
circles.current.push(circle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearContext = () => {
|
||||
if (context.current) {
|
||||
context.current.clearRect(
|
||||
0,
|
||||
0,
|
||||
canvasSize.current.w,
|
||||
canvasSize.current.h,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const drawParticles = () => {
|
||||
clearContext();
|
||||
const particleCount = quantity;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const circle = circleParams();
|
||||
drawCircle(circle);
|
||||
}
|
||||
};
|
||||
|
||||
const remapValue = (
|
||||
value: number,
|
||||
start1: number,
|
||||
end1: number,
|
||||
start2: number,
|
||||
end2: number,
|
||||
): number => {
|
||||
const remapped =
|
||||
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
clearContext();
|
||||
circles.current.forEach((circle: Circle, i: number) => {
|
||||
// Handle the alpha value
|
||||
const edge = [
|
||||
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||
];
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||
const remapClosestEdge = parseFloat(
|
||||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||
);
|
||||
if (remapClosestEdge > 1) {
|
||||
circle.alpha += 0.02;
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
circle.x += circle.dx + vx;
|
||||
circle.y += circle.dy + vy;
|
||||
circle.translateX +=
|
||||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
|
||||
ease;
|
||||
circle.translateY +=
|
||||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
|
||||
ease;
|
||||
|
||||
drawCircle(circle, true);
|
||||
|
||||
// circle gets out of the canvas
|
||||
if (
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.current.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.current.h + circle.size
|
||||
) {
|
||||
// remove the circle from the array
|
||||
circles.current.splice(i, 1);
|
||||
// create a new circle
|
||||
const newCircle = circleParams();
|
||||
drawCircle(newCircle);
|
||||
// update the circle position
|
||||
}
|
||||
});
|
||||
window.requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("pointer-events-none", className)}
|
||||
ref={canvasContainerRef}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas ref={canvasRef} className="size-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Particles;
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
@@ -1,160 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
@@ -1,140 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -1,57 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub, FaStar } from "react-icons/fa";
|
||||
import { buttonVariants } from "./button";
|
||||
import NumberTicker from "./number-ticker";
|
||||
|
||||
export default function StarOnGithubButton() {
|
||||
const [stars, setStars] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStars = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/community-scripts/${basePath}`,
|
||||
{
|
||||
next: { revalidate: 60 * 60 * 24 },
|
||||
},
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setStars(data.stargazers_count || stars);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stars:", error);
|
||||
}
|
||||
};
|
||||
fetchStars();
|
||||
}, [stars]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants(),
|
||||
"hidden h-9 min-w-[240px] gap-2 overflow-hidden whitespace-pre sm:flex lg:flex",
|
||||
"group relative justify-center gap-2 rounded-md transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-2",
|
||||
)}
|
||||
target="_blank"
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
>
|
||||
<span className="absolute right-0 -mt-12 h-32 translate-x-12 rotate-12 bg-white opacity-10 transition-all duration-1000 ease-out group-hover:-translate-x-40" />
|
||||
<div className="flex items-center">
|
||||
<FaGithub className="size-4" />
|
||||
<span className="ml-1">Star on GitHub</span>{" "}
|
||||
</div>
|
||||
<div className="ml-2 flex items-center gap-1 text-sm md:flex">
|
||||
<FaStar className="size-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" />
|
||||
<NumberTicker
|
||||
value={stars}
|
||||
className="font-display font-medium text-white dark:text-black"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "./button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme: currentTheme } = useTheme();
|
||||
|
||||
const handleChangeTheme = (theme: "light" | "dark") => {
|
||||
if (theme === currentTheme) return;
|
||||
|
||||
if (!document.startViewTransition) return setTheme(theme);
|
||||
document.startViewTransition(() => setTheme(theme));
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="icon"
|
||||
className="px-2"
|
||||
aria-label="Toggle theme"
|
||||
onClick={() =>
|
||||
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
>
|
||||
<SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
||||
<MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
Theme Toggle
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||
@@ -1,65 +0,0 @@
|
||||
import { OperatingSystem } from "@/lib/types";
|
||||
import { MessagesSquare, Scroll } from "lucide-react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
|
||||
export const basePath = process.env.BASE_PATH;
|
||||
|
||||
export const navbarLinks = [
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}`,
|
||||
event: "Github",
|
||||
icon: <FaGithub className="h-4 w-4" />,
|
||||
text: "Github",
|
||||
},
|
||||
{
|
||||
href: `https://discord.gg/2wvnMDgdnU`,
|
||||
event: "Discord",
|
||||
icon: <FaDiscord className="h-4 w-4" />,
|
||||
text: "Discord",
|
||||
},
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}/blob/main/CHANGELOG.md`,
|
||||
event: "Change Log",
|
||||
icon: <Scroll className="h-4 w-4" />,
|
||||
text: "Change Log",
|
||||
},
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}/discussions`,
|
||||
event: "Discussions",
|
||||
icon: <MessagesSquare className="h-4 w-4 hidden sm:block" />,
|
||||
text: "Discussions",
|
||||
},
|
||||
];
|
||||
|
||||
export const mostPopularScripts = [
|
||||
"Proxmox VE Post Install",
|
||||
"Docker",
|
||||
"Home Assistant OS",
|
||||
];
|
||||
|
||||
export const analytics = {
|
||||
url: "analytics.proxmoxve-scripts.com",
|
||||
token: "b60d3032-1a11-4244-a100-81d26c5c49a7",
|
||||
};
|
||||
|
||||
export const AlertColors = {
|
||||
warning: "border-red-500/25 bg-destructive/25",
|
||||
info: "border-cyan-500/25 bg-cyan-50 dark:border-cyan-900 dark:bg-cyan-900/25",
|
||||
};
|
||||
|
||||
export const OperatingSystems: OperatingSystem[] = [
|
||||
{
|
||||
name: "Debian",
|
||||
versions: [
|
||||
{ name: "11", slug: "bullseye" },
|
||||
{ name: "12", slug: "bookworm" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Ubuntu",
|
||||
versions: [
|
||||
{ name: "22.04", slug: "jammy" },
|
||||
{ name: "24.04", slug: "noble" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Category } from "./types";
|
||||
|
||||
export const fetchCategories = async () => {
|
||||
const response = await fetch("api/categories");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch categories: ${response.statusText}`);
|
||||
}
|
||||
const categories: Category[] = await response.json();
|
||||
return categories;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export function extractDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
|
||||
export type Script = {
|
||||
name: string;
|
||||
slug: string;
|
||||
categories: number[];
|
||||
date_created: string;
|
||||
type: "vm" | "ct" | "misc";
|
||||
updateable: boolean;
|
||||
privileged: boolean;
|
||||
interface_port: number | null;
|
||||
documentation: string | null;
|
||||
website: string | null;
|
||||
logo: string | null;
|
||||
description: string;
|
||||
install_methods: {
|
||||
type: "default" | "alpine";
|
||||
script: string;
|
||||
resources: {
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
hdd: number | null;
|
||||
os: string | null;
|
||||
version: string | null;
|
||||
};
|
||||
}[];
|
||||
default_credentials: {
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
notes: [
|
||||
{
|
||||
text: string;
|
||||
type: keyof typeof AlertColors;
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export type Category = {
|
||||
name: string;
|
||||
id: number;
|
||||
sort_order: number;
|
||||
scripts: Script[];
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
export interface Version {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface OperatingSystem {
|
||||
name: string;
|
||||
versions: Version[];
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 220.9 39.3% 11%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 224 71.4% 4.1%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 210 20% 98%;
|
||||
--primary-foreground: 220.9 39.3% 11%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 216 12.2% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
* {
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.25);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.glass {
|
||||
backdrop-filter: blur(15px) saturate(100%);
|
||||
-webkit-backdrop-filter: blur(15px) saturate(100%);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const svgToDataUri = require("mini-svg-data-uri");
|
||||
|
||||
const {
|
||||
default: flattenColorPalette,
|
||||
} = require("tailwindcss/lib/util/flattenColorPalette");
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
shine: {
|
||||
from: { backgroundPosition: "200% 0" },
|
||||
to: { backgroundPosition: "-200% 0" },
|
||||
},
|
||||
gradient: {
|
||||
to: {
|
||||
backgroundPosition: "var(--bg-size) 0",
|
||||
},
|
||||
},
|
||||
"shine-pulse": {
|
||||
"0%": {
|
||||
"background-position": "0% 0%",
|
||||
},
|
||||
"50%": {
|
||||
"background-position": "100% 100%",
|
||||
},
|
||||
to: {
|
||||
"background-position": "0% 0%",
|
||||
},
|
||||
},
|
||||
moveHorizontal: {
|
||||
"0%": {
|
||||
transform: "translateX(-50%) translateY(-10%)",
|
||||
},
|
||||
"50%": {
|
||||
transform: "translateX(50%) translateY(10%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateX(-50%) translateY(-10%)",
|
||||
},
|
||||
},
|
||||
moveInCircle: {
|
||||
"0%": {
|
||||
transform: "rotate(0deg)",
|
||||
},
|
||||
"50%": {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "rotate(360deg)",
|
||||
},
|
||||
},
|
||||
moveVertical: {
|
||||
"0%": {
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
"50%": {
|
||||
transform: "translateY(50%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
shine: "shine 8s ease-in-out infinite",
|
||||
gradient: "gradient 8s linear infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require(`tailwindcss-animated`),
|
||||
require("tailwindcss-animate"),
|
||||
addVariablesForColors,
|
||||
function ({ matchUtilities, theme }: any) {
|
||||
matchUtilities(
|
||||
{
|
||||
"bg-grid": (value: any) => ({
|
||||
backgroundImage: `url("${svgToDataUri(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
|
||||
)}")`,
|
||||
}),
|
||||
"bg-grid-small": (value: any) => ({
|
||||
backgroundImage: `url("${svgToDataUri(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
|
||||
)}")`,
|
||||
}),
|
||||
"bg-dot": (value: any) => ({
|
||||
backgroundImage: `url("${svgToDataUri(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`,
|
||||
)}")`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: flattenColorPalette(theme("backgroundColor")),
|
||||
type: "color",
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
} satisfies Config;
|
||||
|
||||
function addVariablesForColors({ addBase, theme }: any) {
|
||||
let allColors = flattenColorPalette(theme("colors"));
|
||||
let newVars = Object.fromEntries(
|
||||
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
|
||||
);
|
||||
addBase({
|
||||
":root": newVars,
|
||||
});
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"next.config.mjs",
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["src/__tests__/setupTests.ts"]
|
||||
},
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "2FAuth",
|
||||
"slug": "2fauth",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2024-12-20",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": null,
|
||||
"website": "https://docs.2fauth.app/",
|
||||
"logo": "https://raw.githubusercontent.com/Bubka/2FAuth/refs/heads/master/public/logo.svg",
|
||||
"description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop. It aims to ease you perform your 2FA authentication steps whatever the device you handle, with a clean and suitable interface.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/2fauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Database credentials: `cat ~/2FAuth.creds`",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The very first account created is automatically set up as an administrator account.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "5etools",
|
||||
"slug": "5etools",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2025-01-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://wiki.tercept.net/en/5eTools",
|
||||
"website": "https://5e.tools/",
|
||||
"logo": "https://wiki.tercept.net/core-wiki-assets/5etoolslogocircle.png",
|
||||
"description": "5eTools is a website providing a suite of tools for 5th Edition Dungeons & Dragons players and Dungeon Masters.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/5etools.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 13,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "Actual Budget",
|
||||
"slug": "actualbudget",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 5006,
|
||||
"documentation": "https://github.com/community-scripts/ProxmoxVE/discussions/807",
|
||||
"website": "https://actualbudget.org/",
|
||||
"logo": "https://raw.githubusercontent.com/actualbudget/actual/master/packages/desktop-client/public/maskable-512x512.png",
|
||||
"description": "Actual Budget is a super fast and privacy-focused app for managing your finances. At its heart is the well proven and much loved Envelope Budgeting methodology.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/actualbudget.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "Proxmox VE LXC IP-Tag",
|
||||
"slug": "add-lxc-iptag",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"date_created": "2024-12-16",
|
||||
"type": "misc",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://raw.githubusercontent.com/home-assistant/brands/master/core_integrations/proxmoxve/icon.png",
|
||||
"description": "This script automatically adds IP address as tags to LXC containers using a Systemd service. The service also updates the tags if a LXC IP address is changed.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "misc/add-lxc-iptag.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within the Proxmox shell",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Configuration: `nano /opt/lxc-iptag/iptag.conf`. iptag.service must be restarted after change.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The Proxmox Node must contain ipcalc and net-tools. `apt-get install -y ipcalc net-tools`",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "NetBird",
|
||||
"slug": "add-netbird-lxc",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-05-19",
|
||||
"type": "misc",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.netbird.io/",
|
||||
"website": "https://netbird.io/",
|
||||
"logo": "https://avatars.githubusercontent.com/u/100464677?s=400&v=4",
|
||||
"description": "NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "misc/add-netbird-lxc.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After the script finishes, reboot the LXC then run `netbird up` in the LXC console",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Execute within the Proxmox main shell",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The script only works in Debian/Ubuntu, not in Alpine!",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "Tailscale",
|
||||
"slug": "add-tailscale-lxc",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "misc",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": "https://tailscale.com/",
|
||||
"logo": "https://avatars.githubusercontent.com/u/48932923?v=4&s=100",
|
||||
"description": "Tailscale is a software-defined networking solution that enables secure communication between devices over the internet. It creates a virtual private network (VPN) that enables devices to communicate with each other as if they were on the same local network. Tailscale works even when the devices are separated by firewalls or subnets, and provides secure and encrypted communication between devices. With Tailscale, users can connect devices, servers, computers, and cloud instances to create a secure network, making it easier to manage and control access to resources. Tailscale is designed to be easy to set up and use, providing a streamlined solution for secure communication between devices over the internet.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "misc/add-tailscale-lxc.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After the script finishes, reboot the LXC then run `tailscale up` in the LXC console",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Execute within the Proxmox shell",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "AdGuard Home",
|
||||
"slug": "adguard",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2024-04-28",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started",
|
||||
"website": "https://adguard.com/en/adguard-home/overview.html",
|
||||
"logo": "https://raw.githubusercontent.com/home-assistant/brands/master/core_integrations/adguard/icon.png",
|
||||
"description": "AdGuard Home is an open-source, self-hosted network-wide ad blocker. It blocks advertisements, trackers, phishing and malware websites, and provides protection against online threats. AdGuard Home is a DNS-based solution, which means it blocks ads and malicious content at the network level, before it even reaches your device. It runs on your home network and can be easily configured and managed through a web-based interface. It provides detailed statistics and logs, allowing you to see which websites are being blocked, and why. AdGuard Home is designed to be fast, lightweight, and easy to use, making it an ideal solution for home users who want to block ads, protect their privacy, and improve the speed and security of their online experience.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/adguard.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Adguard Home can be updated via the user interface.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "AdventureLog",
|
||||
"slug": "adventurelog",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2024-10-26",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": null,
|
||||
"website": "https://adventurelog.app/",
|
||||
"logo": "https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/documentation/static/img/favicon.png",
|
||||
"description": "Adventure Log is an app designed to track outdoor activities and personal achievements, allowing users to log their adventures with photos, notes, and location data. It focuses on enhancing outdoor experiences by preserving memories and sharing them with others.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/adventurelog.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 7,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "AdventureLog uses an initial local IP, if you change your LXC-IP, you need to change the IP here: `/opt/adventurelog/backend/server/.env` and here: `/opt/adventurelog/frontend/.env`",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "AgentDVR",
|
||||
"slug": "agentdvr",
|
||||
"categories": [
|
||||
17
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": true,
|
||||
"interface_port": 8090,
|
||||
"documentation": null,
|
||||
"website": "https://www.ispyconnect.com/",
|
||||
"logo": "https://ispycontent.azureedge.net/img/ispy2.png?raw=true",
|
||||
"description": "AgentDVR a new video surveillance solution for the Internet Of Things.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/agentdvr.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "ubuntu",
|
||||
"version": "22.04"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "All Templates",
|
||||
"slug": "all-templates",
|
||||
"categories": [
|
||||
10
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "misc",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/proxmox.svg",
|
||||
"description": "A script designed to allow for the creation of one of the many free LXC templates. Great for creating system LXCs.\r\nThe script creates a `*.creds` file in the Proxmox root directory with the password of the newly created LXC.\r\nPlease take note that if you plan to use this script for creating TurnKey LXCs, you'll need to modify the hostname after creation.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "misc/all-templates.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Resource and network settings are adjustable post LXC creation.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Alpine",
|
||||
"slug": "alpine",
|
||||
"categories": [
|
||||
9
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": "https://www.alpinelinux.org/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/alpinelinux.svg",
|
||||
"description": "A security-oriented, lightweight Linux distribution based on musl and BusyBox.\r\nBy default, the root password is set to alpine. If you choose to use advanced settings, you will need to define a password, autologin is currently unavailable.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/alpine.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 0.1,
|
||||
"os": "alpine",
|
||||
"version": "3.19"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": "alpine"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "To Update Alpine: `apk update && apk upgrade`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Apache-Cassandra",
|
||||
"slug": "apache-cassandra",
|
||||
"categories": [
|
||||
5
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": "https://cassandra.apache.org/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/apachecassandra.svg",
|
||||
"description": "Apache-Cassandra is an open source NoSQL distributed database trusted by thousands of companies for scalability and high availability without compromising performance.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/apache-cassandra.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Apache-Cassandra Configuration: `nano /etc/cassandra/cassandra.yaml`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Apache-CouchDB",
|
||||
"slug": "apache-couchdb",
|
||||
"categories": [
|
||||
5
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 5984,
|
||||
"documentation": null,
|
||||
"website": "https://couchdb.apache.org/",
|
||||
"logo": "https://couchdb.apache.org/image/couch@2x.png",
|
||||
"description": "Apache-CouchDB Seamless multi-master sync, that scales from Big Data to Mobile, with an Intuitive HTTP/JSON API and designed for Reliability.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/apache-couchdb.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 10,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Show Login Credentials: `cat CouchDB.creds`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "Apache Guacamole",
|
||||
"slug": "apache-guacamole",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-12-19",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 8080,
|
||||
"documentation": null,
|
||||
"website": "https://guacamole.apache.org/",
|
||||
"logo": "https://guacamole.apache.org/images/logos/guac-tricolor-logo.svg",
|
||||
"description": "Apache Guacamole is a clientless remote desktop gateway. It supports standard protocols like VNC, RDP, and SSH.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/apache-guacamole.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "guacadmin",
|
||||
"password": "guacadmin"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "Apt-Cacher-NG",
|
||||
"slug": "apt-cacher-ng",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 3142,
|
||||
"documentation": null,
|
||||
"website": "https://www.unix-ag.uni-kl.de/~bloch/acng/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/linux.svg",
|
||||
"description": "Apt-Cacher-NG is a caching proxy. Specialized for package files from Linux distributors, primarily for Debian (and Debian based) distributions.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/apt-cacher-ng.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "ArchiveBox",
|
||||
"slug": "archivebox",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2024-10-19",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": "https://archivebox.io/",
|
||||
"logo": "https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/refs/heads/dev/website/icon.png",
|
||||
"description": "ArchiveBox is an open source tool that lets organizations & individuals archive both public & private web content while retaining control over their data. It can be used to save copies of bookmarks, preserve evidence for legal cases, backup photos from FB/Insta/Flickr or media from YT/Soundcloud/etc., save research papers, and more...",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/archivebox.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "archivebox",
|
||||
"password": "helper-scripts.com"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Aria2",
|
||||
"slug": "aria2",
|
||||
"categories": [
|
||||
16
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 6880,
|
||||
"documentation": null,
|
||||
"website": "https://aria2.github.io/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/linux.svg",
|
||||
"description": "Aria2 is a lightweight multi-protocol & multi-source, cross platform download utility operated in command-line. It supports HTTP/HTTPS, FTP, SFTP, BitTorrent and Metalink.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/aria2.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Within the LXC console, run `cat rpc.secret` to display the rpc-secret. Copy this token and paste it into the Aria2 RPC Secret Token box within the AriaNG Settings. Then, click the reload AriaNG button.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "Audiobookshelf",
|
||||
"slug": "audiobookshelf",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 13378,
|
||||
"documentation": "https://www.audiobookshelf.org/guides/",
|
||||
"website": "https://www.audiobookshelf.org/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/audiobookshelf.svg",
|
||||
"description": "Audiobookshelf is a Self-hosted audiobook and podcast server.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/audiobookshelf.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "authentik",
|
||||
"slug": "authentik",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-12-27",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 9000,
|
||||
"documentation": "https://docs.goauthentik.io/docs/",
|
||||
"website": "https://goauthentik.io/",
|
||||
"logo": "https://raw.githubusercontent.com/goauthentik/authentik/refs/heads/main/website/static/img/icon.png",
|
||||
"description": "authentik is an IdP (Identity Provider) and SSO (single sign on) that is built with security at the forefront of every piece of code, every feature, with an emphasis on flexibility and versatility.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/authentik.sh",
|
||||
"resources": {
|
||||
"cpu": 6,
|
||||
"ram": 8192,
|
||||
"hdd": 12,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Authentik is very resource-heavy, it is recommended to use at least 8GB RAM anytime!",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user