mirror of
https://github.com/community-scripts/ProxmoxVED.git
synced 2026-02-25 15:25:53 +00:00
Merge branch 'main' of https://github.com/omiinaya/ProxmoxVED into kasm
This commit is contained in:
1888
frontend/package-lock.json
generated
1888
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,70 +20,71 @@
|
||||
"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",
|
||||
"chart.js": "^4.4.1",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.71.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.11.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"next": "15.2.3",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.3.0",
|
||||
"nuqs": "^2.1.1",
|
||||
"pocketbase": "^0.21.4",
|
||||
"nuqs": "^2.4.1",
|
||||
"pocketbase": "^0.21.5",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"react": "19.0.0-rc-02c0e824-20241028",
|
||||
"react": "19.0.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-datepicker": "^7.6.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-icons": "^5.5.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"
|
||||
"simple-icons": "^13.21.0",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.68.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@types/node": "^22",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.13.16",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-animated": "^1.1.2",
|
||||
"typescript": "^5",
|
||||
"vite-tsconfig-paths": "^5.1.3",
|
||||
"vitest": "^3.0.8"
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
|
||||
49
frontend/public/json/add-iptag.json
Normal file
49
frontend/public/json/add-iptag.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "Proxmox VE LXC IP-Tag",
|
||||
"slug": "add-lxc-iptag",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"date_created": "2025-04-02",
|
||||
"type": "addon",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/proxmox.svg",
|
||||
"config_path": "",
|
||||
"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": "tools/addon/add-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/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
frontend/public/json/allstarlink.json
Normal file
40
frontend/public/json/allstarlink.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "AllStarLink",
|
||||
"slug": "allstarlink-vm",
|
||||
"categories": [
|
||||
24
|
||||
],
|
||||
"date_created": "2025-05-05",
|
||||
"type": "vm",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://allstarlink.github.io/",
|
||||
"website": "https://www.allstarlink.org/",
|
||||
"logo": "https://raw.githubusercontent.com/AllStarLink/ASL3-Manual/blob/main/docs/assets/allstar-logo-small.png",
|
||||
"config_path": "",
|
||||
"description": "AllStarLink is a network of Amateur Radio repeaters, remote base stations and hot spots accessible to each other via Voice over Internet Protocol.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "vm/allstarlink-vm.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Options to Install Allmon3",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/babybuddy.json
Normal file
40
frontend/public/json/babybuddy.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Baby Buddy",
|
||||
"slug": "babybuddy",
|
||||
"categories": [
|
||||
23
|
||||
],
|
||||
"date_created": "2025-05-16",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://docs.baby-buddy.net/",
|
||||
"website": "https://github.com/babybuddy/babybuddy",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/baby-buddy.svg",
|
||||
"config_path": "/opt/babybuddy/babybuddy/production.py",
|
||||
"description": "Baby Buddy is an open-source web application designed to assist caregivers in tracking various aspects of a baby's daily routine, including sleep, feedings, diaper changes, tummy time, and more. By recording this data, caregivers can better understand and anticipate their baby's needs, reducing guesswork in daily care. The application offers a user-friendly dashboard for data entry and visualization, supports multiple users, and provides features like timers and reminders. Additionally, Baby Buddy can be integrated with platforms like Home Assistant and Grafana for enhanced functionality.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/babybuddy.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 5,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "for private SSL setup visit: `https://github.com/babybuddy/babybuddy/blob/master/docs/setup/ssl.md`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/backrest.json
Normal file
40
frontend/public/json/backrest.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Backrest",
|
||||
"slug": "backrest",
|
||||
"categories": [
|
||||
7
|
||||
],
|
||||
"date_created": "2025-05-11",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 9898,
|
||||
"documentation": "https://garethgeorge.github.io/backrest/introduction/getting-started",
|
||||
"website": "https://garethgeorge.github.io/backrest",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/backrest-light.svg",
|
||||
"config_path": "/opt/backrest/config/config.json",
|
||||
"description": "Backrest is a web-accessible backup solution built on top of restic and providing a WebUI which wraps the restic CLI and makes it easy to create repos, browse snapshots, and restore files. Additionally, Backrest can run in the background and take an opinionated approach to scheduling snapshots and orchestrating repo health operations.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/backrest.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "`cat ~/.ssh/id_ed25519.pub` to view ssh public key. This key is used to authenticate with sftp targets. You can add this key on the sftp server."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "Calibre-Web-Automated",
|
||||
"slug": "calibre-web-automated",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2025-03-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8083,
|
||||
"documentation": null,
|
||||
"website": "https://github.com/crocodilestick/Calibre-Web-Automated",
|
||||
"logo": "https://sasquatters.com/media/2017/04/Calibre-web-banner-768x512.jpg",
|
||||
"description": "Calibre-Web but automated and with Calibre features! Fully automate and simplify your eBook set up!",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/calibre-web-automated.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "This LXC is not interchangeable with the Calibre-Web LXC",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Options enabled by default: Kobo sync; Goodreads author info; metadata extaction from epub, fb2, pdf; cover extraction from cbr, cbz, cbt files",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/cloudflare-ddns.json
Normal file
44
frontend/public/json/cloudflare-ddns.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Cloudflare-DDNS",
|
||||
"slug": "cloudflare-ddns",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2025-04-23",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://github.com/favonia/cloudflare-ddns/blob/main/README.markdown",
|
||||
"config_path": "/etc/systemd/system/cloudflare-ddns.service",
|
||||
"website": "https://github.com/favonia/cloudflare-ddns",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/cloudflare.svg",
|
||||
"description": "A feature-rich and robust Cloudflare DDNS updater with a small footprint. The program will detect your machine’s public IP addresses and update DNS records using the Cloudflare API",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/cloudflare-ddns.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "To setup the updater you must have the Cloudflare Token and the domains, please read the Github documentation at \"Step 1: Updating the Compose File\" (only the expandable section)",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "To update the configuration edit `/etc/systemd/system/cloudflare-ddns.service`. After edit please restard with `sudo systemctl restart cloudflare-ddns.service`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "FileFlows",
|
||||
"slug": "fileflows",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2025-03-07",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 19200,
|
||||
"documentation": "https://fileflows.com/docs",
|
||||
"website": "https://fileflows.com/",
|
||||
"logo": "https://raw.githubusercontent.com/revenz/FileFlows/refs/heads/develop/icon.png",
|
||||
"description": "FileFlows is a powerful, open-source tool for automating media file processing workflows, including encoding, decoding, and media management. It offers an intuitive GUI and extensive plugin support, making it ideal for tasks like video transcoding, organizing, and managing large media libraries.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/fileflows.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "With Privileged/Unprivileged Hardware Acceleration Support",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,15 +12,16 @@
|
||||
"documentation": "https://sangomakb.atlassian.net/wiki/spaces/FP/overview?homepageId=8454359",
|
||||
"website": "https://www.freepbx.org/",
|
||||
"logo": "https://avatars.githubusercontent.com/u/696423?s=200&v=4",
|
||||
"config_path": "",
|
||||
"description": "FreePBX is a web-based open-source graphical user interface that manages Asterisk, a voice over IP and telephony server.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/freepbx.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 1024,
|
||||
"hdd": 20,
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 10,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
@@ -30,5 +31,11 @@
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
"notes": [
|
||||
{
|
||||
"text": "This script uses the official FreePBX install script. Check it here: https://github.com/FreePBX/sng_freepbx_debian_install",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
35
frontend/public/json/homarr.json
Normal file
35
frontend/public/json/homarr.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Homarr",
|
||||
"slug": "homarr",
|
||||
"categories": [
|
||||
10
|
||||
],
|
||||
"date_created": "2025-05-08",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 7575,
|
||||
"documentation": null,
|
||||
"website": "https://homarr.dev/",
|
||||
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/homarr.svg",
|
||||
"config_path": "/opt/homarr/.env",
|
||||
"description": "Homarr is a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/homarr.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
41
frontend/public/json/immich.json
Normal file
41
frontend/public/json/immich.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Immich",
|
||||
"slug": "immich",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2025-03-30",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 2283,
|
||||
"documentation": "https://immich.app/docs/overview/introduction",
|
||||
"website": "https://immich.app",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/immich.png",
|
||||
"config_path": "/opt/immich/.env",
|
||||
"description": "High performance self-hosted photo and video management solution.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/immich.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 12,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "During installation, HW-accelerated machine-learning is an available option. This also allows for HW-accelerated transcoding, but it must be enabled in Video Transcoding Settings",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
35
frontend/public/json/jitsi-meet.json
Normal file
35
frontend/public/json/jitsi-meet.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "jitsi-meet",
|
||||
"slug": "jitsi-meet",
|
||||
"categories": [
|
||||
18
|
||||
],
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 443,
|
||||
"documentation": "https://jitsi.github.io/handbook/docs/intro",
|
||||
"website": "https://jitsi.org/jitsi-meet/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/jitsi-meet.webp",
|
||||
"config_path": "/etc/jitsi/meet/[your-hostname]-config.js",
|
||||
"description": "Go ahead, video chat with the whole team. In fact, invite everyone you know. Jitsi Meet is a fully encrypted, 100% open source video conferencing solution that you can use all day, every day, for free — with no account needed.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/jitsi-meet.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/jumpserver.json
Normal file
35
frontend/public/json/jumpserver.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "JumpServer",
|
||||
"slug": "jumpserver",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2025-05-05",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://www.jumpserver.com/docs",
|
||||
"website": "https://www.jumpserver.com/",
|
||||
"logo": "https://raw.githubusercontent.com/jumpserver/jumpserver/refs/heads/dev/apps/static/img/logo.png",
|
||||
"config_path": "",
|
||||
"description": "JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/jumpserver.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 8192,
|
||||
"hdd": 60,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "ChangeMe"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"documentation": "https://docs.librenms.org/",
|
||||
"website": "https://librenms.org/",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/librenms.svg",
|
||||
"config_path": "/opt/librenms/config.php and /opt/librenms/.env",
|
||||
"description": "LibreNMS is an open-source, community-driven network monitoring system that provides automatic discovery, alerting, and performance tracking for network devices. It supports a wide range of hardware and integrates with various notification and logging platforms.",
|
||||
"install_methods": [
|
||||
{
|
||||
@@ -27,8 +28,9 @@
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
frontend/public/json/librespeed.json
Normal file
35
frontend/public/json/librespeed.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Librespeed",
|
||||
"slug": "librespeed",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2025-04-26",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://github.com/librespeed/speedtest/blob/master/doc.md",
|
||||
"config_path": "",
|
||||
"website": "https://librespeed.org",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/librespeed.svg",
|
||||
"description": "No Flash, No Java, No Websocket, No Bullshit. This is a very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/librespeed.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"documentation": "https://manyfold.app/sysadmin/",
|
||||
"website": "https://manyfold.app/",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/webp/manyfold.webp",
|
||||
"config_path": "",
|
||||
"description": "Manyfold is an open source, self-hosted web application for managing a collection of 3d models, particularly focused on 3d printing.",
|
||||
"install_methods": [
|
||||
{
|
||||
@@ -32,3 +33,4 @@
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Meilisearch",
|
||||
"slug": "meilisearch",
|
||||
"categories": [
|
||||
8
|
||||
],
|
||||
"date_created": "2025-03-21",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 7700,
|
||||
"documentation": "https://www.meilisearch.com/docs",
|
||||
"website": "https://www.meilisearch.com/",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/meilisearch.svg",
|
||||
"description": "Meilisearch is a fast, open-source search engine designed for instant, full-text search with typo tolerance. It provides an API that allows developers to integrate powerful search features into applications. Meilisearch-UI is an optional web-based interface that provides a simple way to interact with Meilisearch, visualize indexed data, and test queries without needing to use the API directly.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/meilisearch.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 5,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Meilisearch-UI can optionally be installed for a web-based search interface",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -28,3 +28,4 @@
|
||||
{ "name": "Miscellaneous", "id": 0, "sort_order": 99.0, "description": "General scripts and tools that don't fit into other categories." }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "OpenProject",
|
||||
"slug": "openproject",
|
||||
"categories": [
|
||||
25
|
||||
],
|
||||
"date_created": "2025-03-21",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://www.openproject.org",
|
||||
"website": "https://www.openproject.org",
|
||||
"logo": "https://raw.githubusercontent.com/opf/openproject/dev/docker/prod/logo.png",
|
||||
"description": "OpenProject is a web-based project management software. Use OpenProject to manage your projects, tasks and goals. Collaborate via work packages and link them to your pull requests on Github. Read more about the OpenProject GitHub integration.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/openproject.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "openziti-tunnel",
|
||||
"slug": "openziti-tunnel",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2025-03-20",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://openziti.io/docs/reference/tunnelers/docker/",
|
||||
"website": "https://www.openziti.io/",
|
||||
"logo": "https://raw.githubusercontent.com/openziti/ziti-doc/main/docusaurus/static/img/ziti-logo-dark.svg",
|
||||
"description": "OpenZiti is an open-source, zero trust networking platform that enables secure connectivity between applications, services, and devices. It provides secure, encrypted connections between clients and services, and can be used to create secure, zero trust networks.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/openziti-tunnel.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "Ubuntu",
|
||||
"version": "24.04"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "The Openziti tunnel is installed in host mode; please see documentation for more information",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Openziti tunnel prompts for identity enrollment token during installation",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/phpmyadmin.json
Normal file
40
frontend/public/json/phpmyadmin.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "PhpMyAdmin",
|
||||
"slug": "phpmyadmin",
|
||||
"categories": [
|
||||
8
|
||||
],
|
||||
"date_created": "2025-04-29",
|
||||
"type": "addon",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://www.phpmyadmin.net/docs/",
|
||||
"config_path": "Debian/Ubuntu: /var/www/html/phpMyAdmin | Alpine: /usr/share/phpmyadmin",
|
||||
"website": "https://www.phpmyadmin.net/",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/webp/phpmyadmin-light.webp",
|
||||
"description": "phpMyAdmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB. Frequently used operations (managing databases, tables, columns, relations, indexes, users, permissions, etc) can be performed via the user interface, while you still have the ability to directly execute any SQL statement.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "tools/addon/phpmyadmin.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within an existing LXC Console",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/polaris.json
Normal file
35
frontend/public/json/polaris.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Polaris",
|
||||
"slug": "polaris",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2025-03-25",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://github.com/agersant/polaris/blob/master/docs/SETUP.md",
|
||||
"website": "https://github.com/agersant/polaris",
|
||||
"logo": "https://raw.githubusercontent.com/agersant/polaris/refs/heads/master/res/branding/logo/sticker_print.svg",
|
||||
"config_path": "",
|
||||
"description": "Polaris is a self-hosted music streaming server that allows you to enjoy your personal music collection from any computer or mobile device. It is a free and open-source application with no premium version. Polaris is highly performant and responsive, supporting large music libraries with over 100,000 tracks. It features an intuitive user interface and supports various audio formats, including FLAC, MP3, MP4, OGG, and WAV. There is an Android client available through the Google Play Store, F-Droid, or GitHub Releases.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/polaris.sh",
|
||||
"resources": {
|
||||
"cpu": 3,
|
||||
"ram": 2048,
|
||||
"hdd": 7,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "qBittorrent",
|
||||
"slug": "qbittorrent",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 8090,
|
||||
"documentation": null,
|
||||
"website": "https://www.qbittorrent.org/",
|
||||
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/qbittorrent.svg",
|
||||
"description": "qBittorrent offers a user-friendly interface that allows users to search for and download torrent files easily. It also supports magnet links, which allow users to start downloading files without the need for a torrent file.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/qbittorrent.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "changeme"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
44
frontend/public/json/rclone.json
Normal file
44
frontend/public/json/rclone.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Rclone",
|
||||
"slug": "rclone",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://rclone.org/docs/",
|
||||
"website": "https://rclone.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/rclone.svg",
|
||||
"config_path": "~/.config/rclone/rclone.conf",
|
||||
"description": "Rclone is a command-line program to manage files on cloud storage. It is a feature-rich alternative to cloud vendors' web storage interfaces",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/rclone.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 1,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"type": "info",
|
||||
"text": "`cat ~/rclone.creds` to view login credentials"
|
||||
},
|
||||
{
|
||||
"type": "info",
|
||||
"text": "`htpasswd -b -B /opt/login.pwd newuser newuserpassword` to add more users."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "slskd",
|
||||
"slug": "slskd",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2025-03-11",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 5030,
|
||||
"documentation": "https://github.com/slskd/slskd/tree/master/docs",
|
||||
"website": "https://github.com/slskd/slskd",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/slskd.png",
|
||||
"description": "A modern client-server application for the Soulseek file sharing network. ",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/slskd.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "slskd",
|
||||
"password": "slskd"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "See /opt/slskd/config/sksld.yml to add your Soulseek credentials",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "This LXC includes Soularr; it needs to be configured (/opt/soularr/config.ini) before it will work",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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";
|
||||
@@ -20,21 +14,29 @@ type InstallMethodProps = {
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function InstallMethod({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: InstallMethodProps) {
|
||||
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 { type, slug } = prev;
|
||||
const newMethodType = "default";
|
||||
|
||||
let scriptPath = "";
|
||||
|
||||
if (type === "pve") {
|
||||
scriptPath = `tools/pve/${slug}.sh`;
|
||||
} else if (type === "addon") {
|
||||
scriptPath = `tools/addon/${slug}.sh`;
|
||||
} else {
|
||||
scriptPath = `${type}/${slug}.sh`;
|
||||
}
|
||||
|
||||
const method = InstallMethodSchema.parse({
|
||||
type: "default",
|
||||
script: `${prev.type}/${prev.slug}.sh`,
|
||||
type: newMethodType,
|
||||
script: scriptPath,
|
||||
resources: {
|
||||
cpu: null,
|
||||
ram: null,
|
||||
@@ -43,6 +45,7 @@ function InstallMethod({
|
||||
version: null,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
install_methods: [...prev.install_methods, method],
|
||||
@@ -63,9 +66,7 @@ function InstallMethod({
|
||||
|
||||
if (key === "type") {
|
||||
updatedMethod.script =
|
||||
value === "alpine"
|
||||
? `${prev.type}/alpine-${prev.slug}.sh`
|
||||
: `${prev.type}/${prev.slug}.sh`;
|
||||
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") {
|
||||
@@ -112,10 +113,7 @@ function InstallMethod({
|
||||
<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)}
|
||||
>
|
||||
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
@@ -205,9 +203,7 @@ function InstallMethod({
|
||||
<SelectValue placeholder="Version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.find(
|
||||
(os) => os.name === method.resources.os,
|
||||
)?.versions.map((version) => (
|
||||
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
|
||||
<SelectItem key={version.slug} value={version.name}>
|
||||
{version.name}
|
||||
</SelectItem>
|
||||
@@ -215,22 +211,12 @@ function InstallMethod({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => removeInstallMethod(index)}
|
||||
>
|
||||
<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}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -147,4 +147,4 @@ const NoteItem = memo(
|
||||
NoteItem.displayName = 'NoteItem';
|
||||
|
||||
|
||||
export default memo(Note);
|
||||
export default memo(Note);
|
||||
@@ -24,8 +24,8 @@ export const ScriptSchema = z.object({
|
||||
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'" })
|
||||
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
|
||||
}),
|
||||
updateable: z.boolean(),
|
||||
privileged: z.boolean(),
|
||||
@@ -34,6 +34,7 @@ export const ScriptSchema = z.object({
|
||||
website: z.string().url().nullable(),
|
||||
logo: z.string().url().nullable(),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
config_path: z.string(),
|
||||
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
|
||||
default_credentials: z.object({
|
||||
username: z.string().nullable(),
|
||||
|
||||
@@ -5,18 +5,8 @@ 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 { 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";
|
||||
@@ -42,6 +32,7 @@ const initialScript: Script = {
|
||||
privileged: false,
|
||||
interface_port: null,
|
||||
documentation: null,
|
||||
config_path: "",
|
||||
website: null,
|
||||
logo: null,
|
||||
description: "",
|
||||
@@ -66,29 +57,37 @@ export default function JSONGenerator() {
|
||||
.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 };
|
||||
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) => ({
|
||||
if (updated.slug && updated.type) {
|
||||
updated.install_methods = updated.install_methods.map((method) => {
|
||||
let scriptPath = "";
|
||||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
} else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
} else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
} else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
return {
|
||||
...method,
|
||||
script:
|
||||
method.type === "alpine"
|
||||
? `${updated.type}/alpine-${updated.slug}.sh`
|
||||
: `${updated.type}/${updated.slug}.sh`,
|
||||
}));
|
||||
}
|
||||
script: scriptPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
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));
|
||||
@@ -101,13 +100,13 @@ export default function JSONGenerator() {
|
||||
const jsonString = JSON.stringify(script, null, 2);
|
||||
const blob = new Blob([jsonString], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${script.slug || "script"}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}, [script]);
|
||||
@@ -120,16 +119,13 @@ export default function JSONGenerator() {
|
||||
);
|
||||
|
||||
const formattedDate = useMemo(
|
||||
() =>
|
||||
script.date_created ? format(script.date_created, "PPP") : undefined,
|
||||
() => (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")}
|
||||
>
|
||||
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
|
||||
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isValid
|
||||
@@ -160,21 +156,13 @@ export default function JSONGenerator() {
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Example"
|
||||
value={script.name}
|
||||
onChange={(e) => updateScript("name", e.target.value)}
|
||||
/>
|
||||
<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)}
|
||||
/>
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -197,11 +185,15 @@ export default function JSONGenerator() {
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
categories={categories}
|
||||
/>
|
||||
<div>
|
||||
<Label>Config Path</Label>
|
||||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
||||
/>
|
||||
</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>
|
||||
@@ -209,10 +201,7 @@ export default function JSONGenerator() {
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"pl-3 text-left font-normal w-full",
|
||||
!script.date_created && "text-muted-foreground",
|
||||
)}
|
||||
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" />
|
||||
@@ -230,38 +219,26 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select
|
||||
value={script.type}
|
||||
onValueChange={(value) => updateScript("type", value)}
|
||||
>
|
||||
<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>
|
||||
<SelectItem value="pve">PVE-Tool</SelectItem>
|
||||
<SelectItem value="addon">Add-On</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)
|
||||
}
|
||||
/>
|
||||
<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)
|
||||
}
|
||||
/>
|
||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,12 +246,7 @@ export default function JSONGenerator() {
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={(e) =>
|
||||
updateScript(
|
||||
"interface_port",
|
||||
e.target.value ? Number(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -285,17 +257,10 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("documentation", e.target.value || null)
|
||||
}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
setIsValid={setIsValid}
|
||||
setZodErrors={setZodErrors}
|
||||
/>
|
||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
<h3 className="text-xl font-semibold">Default Credentials</h3>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
@@ -317,34 +282,21 @@ export default function JSONGenerator() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Note
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
setIsValid={setIsValid}
|
||||
setZodErrors={setZodErrors}
|
||||
/>
|
||||
<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">
|
||||
<div className="absolute right-2 top-2 flex gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Button size="icon" variant="outline" onClick={handleCopy}>
|
||||
{isCopied ? <Check className="h-4 w-4" /> : <Clipboard className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Button size="icon" variant="outline" onClick={handleDownload}>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<pre className="mt-4 p-4 bg-secondary rounded shadow overflow-x-scroll">
|
||||
{JSON.stringify(script, null, 2)}
|
||||
</pre>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import QueryProvider from "@/components/query-provider"; // HINZUGEFÜGT
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { analytics, basePath } from "@/config/siteConfig";
|
||||
@@ -81,7 +82,9 @@ export default function RootLayout({
|
||||
<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>
|
||||
<QueryProvider>
|
||||
<NuqsAdapter>{children}</NuqsAdapter>
|
||||
</QueryProvider>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import FAQ from "@/components/FAQ";
|
||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardFooter } from "@/components/ui/card";
|
||||
@@ -34,99 +35,109 @@ export default function Page() {
|
||||
}, [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`,
|
||||
)}
|
||||
>
|
||||
Develop Instance
|
||||
</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="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-3xl font-semibold tracking-tighter md:text-7xl">
|
||||
Beta Scripts
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<p>
|
||||
On this Website you can find a collection of scripts that are under development and only for testing.
|
||||
We do not provide any support for these scripts when run in production, but you can help us by testing them and providing feedback.
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl">
|
||||
Make managing your Homelab a breeze
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<p>
|
||||
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
||||
</p>
|
||||
<p>
|
||||
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you're a seasoned
|
||||
user or a newcomer, we've got you covered.
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="py-20" id="faq">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4">Frequently Asked Questions</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Find answers to common questions about our Proxmox VE scripts
|
||||
</p>
|
||||
</div>
|
||||
<FAQ />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
68
frontend/src/app/random-script/page.tsx
Normal file
68
frontend/src/app/random-script/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
||||
import { Loader2, RefreshCw } from "lucide-react";
|
||||
|
||||
function getRandomScript(categories: Category[]): Script | null {
|
||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
||||
if (allScripts.length === 0) return null;
|
||||
const idx = Math.floor(Math.random() * allScripts.length);
|
||||
return allScripts[idx];
|
||||
}
|
||||
|
||||
export default function RandomScriptPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [randomScript, setRandomScript] = useState<Script | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch categories/scripts on mount
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchCategories()
|
||||
.then((cats) => {
|
||||
setCategories(cats);
|
||||
setRandomScript(getRandomScript(cats));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Handler to re-roll a new random script
|
||||
const reroll = () => {
|
||||
setRandomScript(getRandomScript(categories));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 flex flex-col items-center sm:px-4 xl:px-0">
|
||||
<div className="w-full max-w-5xl flex flex-col items-center">
|
||||
<div className="w-full flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Random Script</h1>
|
||||
<button
|
||||
onClick={reroll}
|
||||
className="flex items-center gap-2 rounded-lg bg-accent/30 px-4 py-2 text-base font-medium hover:bg-accent/50 transition-colors"
|
||||
title="Pick another random script"
|
||||
disabled={loading || categories.length === 0}
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Re-Roll
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center w-full h-64">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
) : randomScript ? (
|
||||
<ScriptItem item={randomScript} setSelectedScript={() => { }} />
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground">
|
||||
No scripts available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
frontend/src/app/scripts/_components/ResourceDisplay.tsx
Normal file
42
frontend/src/app/scripts/_components/ResourceDisplay.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
||||
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
||||
|
||||
interface ResourceDisplayProps {
|
||||
title: string;
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
hdd: number | null;
|
||||
}
|
||||
|
||||
interface IconTextProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function IconText({ icon, label }: IconTextProps) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-accent/20 px-2 py-1 text-sm">
|
||||
{icon}
|
||||
<span className="text-foreground/90">{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps) {
|
||||
const hasCPU = typeof cpu === "number" && cpu > 0;
|
||||
const hasRAM = typeof ram === "number" && ram > 0;
|
||||
const hasHDD = typeof hdd === "number" && hdd > 0;
|
||||
|
||||
if (!hasCPU && !hasRAM && !hasHDD) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">{title}</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{hasCPU && <IconText icon={<CPUIcon />} label={`${cpu} vCPU`} />}
|
||||
{hasRAM && <IconText icon={<RAMIcon />} label={getDisplayValueFromRAM(ram!)} />}
|
||||
{hasHDD && <IconText icon={<HDDIcon />} label={`${hdd} GB`} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,17 +46,17 @@ export default function ScriptAccordion({
|
||||
);
|
||||
if (category) {
|
||||
setExpandedItem(category.name);
|
||||
handleSelected(selectedScript);
|
||||
}
|
||||
}
|
||||
}, [selectedScript, items]);
|
||||
|
||||
}, [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"
|
||||
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden mt-3 p-2"
|
||||
>
|
||||
{items.map((category) => (
|
||||
<AccordionItem
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
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";
|
||||
@@ -23,7 +16,8 @@ export const getDisplayValueFromType = (type: string) => {
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "misc":
|
||||
case "pve":
|
||||
case "addon":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
@@ -35,7 +29,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
|
||||
const latestScripts = useMemo(() => {
|
||||
if (!items) return [];
|
||||
|
||||
|
||||
const scripts = items.flatMap((category) => category.scripts || []);
|
||||
|
||||
// Filter out duplicates by slug
|
||||
@@ -47,8 +41,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
});
|
||||
|
||||
return Array.from(uniqueScriptsMap.values()).sort(
|
||||
(a, b) =>
|
||||
new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
@@ -59,7 +52,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const goToPreviousPage = () => {
|
||||
setPage((prevPage) => prevPage - 1);
|
||||
};
|
||||
|
||||
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = page * ITEMS_PER_PAGE;
|
||||
|
||||
@@ -74,18 +67,12 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
<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}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div onClick={goToNextPage} className="cursor-pointer select-none p-2 text-sm font-semibold">
|
||||
{page === 1 ? "More.." : "Next"}
|
||||
</div>
|
||||
)}
|
||||
@@ -94,10 +81,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
)}
|
||||
<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"
|
||||
>
|
||||
<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 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
@@ -107,10 +91,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -126,9 +107,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground">
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground">{script.description}</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="outline">
|
||||
@@ -151,9 +130,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) =>
|
||||
mostPopularScripts.includes(script.slug),
|
||||
);
|
||||
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
|
||||
@@ -166,10 +143,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
)}
|
||||
<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"
|
||||
>
|
||||
<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 size-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
@@ -179,10 +153,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,168 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Script, AppVersion } from "@/lib/types";
|
||||
import { fetchVersions } from "@/lib/data";
|
||||
import { AppVersion, Script } from "@/lib/types";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { useVersions } from "@/hooks/useVersions";
|
||||
import { cleanSlug } from "@/lib/utils/resource-utils";
|
||||
import { Suspense } from "react";
|
||||
import { ResourceDisplay } from "./ResourceDisplay";
|
||||
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 ConfigFile from "./ScriptItems/ConfigFile";
|
||||
import InterFaces from "./ScriptItems/InterFaces";
|
||||
import Tooltips from "./ScriptItems/Tooltips";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
interface ScriptItemProps {
|
||||
item: Script;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
item: Script;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}
|
||||
|
||||
function ScriptItem({
|
||||
item,
|
||||
setSelectedScript,
|
||||
}: ScriptItemProps) {
|
||||
function ScriptHeader({ item }: { item: Script }) {
|
||||
const defaultInstallMethod = item.install_methods?.[0];
|
||||
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
|
||||
const version = defaultInstallMethod?.resources?.version || "";
|
||||
|
||||
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
const [versions, setVersions] = useState<AppVersion[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchVersions()
|
||||
.then((fetchedVersions) => {
|
||||
console.log("Fetched Versions: ", fetchedVersions);
|
||||
if (Array.isArray(fetchedVersions)) {
|
||||
setVersions(fetchedVersions);
|
||||
} else if (fetchedVersions && typeof fetchedVersions === "object") {
|
||||
setVersions([fetchedVersions]);
|
||||
} else {
|
||||
setVersions([]);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error fetching versions:", error));
|
||||
}, []);
|
||||
|
||||
const defaultInstallMethod = item.install_methods?.[0];
|
||||
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
|
||||
const version = defaultInstallMethod?.resources?.version || "";
|
||||
|
||||
const [linksVisible, setLinksVisible] = useState<boolean>(false);
|
||||
|
||||
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 mb-4">
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Default OS: {os} {version}
|
||||
</p>
|
||||
</div>
|
||||
<div>{versions.length === 0 ? (<p>Loading versions...</p>) :
|
||||
(<>
|
||||
<p className="text-l text-foreground">Version:</p>
|
||||
<p className="text-l text-muted-foreground">{versions.find((v) =>
|
||||
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
|
||||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
|
||||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
|
||||
|
||||
)?.version || "No Version information found"
|
||||
}</p>
|
||||
<p className="text-l text-foreground">Latest changes:</p>
|
||||
<p className="text-l text-muted-foreground">
|
||||
{(() => {
|
||||
const matchedVersion = versions.find((v) =>
|
||||
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
|
||||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
|
||||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
|
||||
);
|
||||
return matchedVersion?.date ?
|
||||
extractDate(matchedVersion.date as unknown as string) :
|
||||
"No date information found"
|
||||
})()}
|
||||
</p>
|
||||
</>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-4 ml-auto">
|
||||
<DefaultSettings item={item} />
|
||||
<InterFaces item={item} />
|
||||
<div>
|
||||
<>
|
||||
<button
|
||||
onClick={() => setLinksVisible(!linksVisible)}
|
||||
className="flex items-right justify-right gap-1 mb-2 rounded-md border border-accent bg-accent/20 px-2 py-1 text-l hover:bg-accent w-30"
|
||||
>
|
||||
Show Links {linksVisible ? '▲' : '▼'}
|
||||
</button>
|
||||
{linksVisible && <Buttons item={item} />}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-6 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-6 flex-grow">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
|
||||
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>
|
||||
);
|
||||
<div className="flex flex-col justify-between flex-grow space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
{item.name}
|
||||
<VersionInfo item={item} />
|
||||
<span className="inline-flex items-center rounded-md bg-accent/30 px-2 py-1 text-sm">
|
||||
{getDisplayValueFromType(item.type)}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span>Added {extractDate(item.date_created)}</span>
|
||||
<span>•</span>
|
||||
<span className=" capitalize">
|
||||
{os} {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <VersionInfo item={item} /> */}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
||||
{defaultInstallMethod?.resources && (
|
||||
<ResourceDisplay
|
||||
title="Default"
|
||||
cpu={defaultInstallMethod.resources.cpu}
|
||||
ram={defaultInstallMethod.resources.ram}
|
||||
hdd={defaultInstallMethod.resources.hdd}
|
||||
/>
|
||||
)}
|
||||
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
|
||||
<ResourceDisplay
|
||||
title="Alpine"
|
||||
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 justify-between">
|
||||
<InterFaces item={item} />
|
||||
<div className="flex justify-end">
|
||||
<Buttons item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScriptItem;
|
||||
function VersionInfo({ item }: { item: Script }) {
|
||||
const { data: versions = [], isLoading } = useVersions();
|
||||
|
||||
if (isLoading || versions.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">Loading versions...</p>;
|
||||
}
|
||||
|
||||
const matchedVersion = versions.find((v: AppVersion) => {
|
||||
const cleanName = v.name.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
||||
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
||||
});
|
||||
|
||||
if (!matchedVersion) return null;
|
||||
|
||||
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
||||
}
|
||||
|
||||
export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border/40 bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm">
|
||||
<div className="p-6 space-y-6">
|
||||
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
||||
<ScriptHeader item={item} />
|
||||
</Suspense>
|
||||
|
||||
<Description item={item} />
|
||||
<Alerts item={item} />
|
||||
|
||||
<div className="mt-4 rounded-lg border shadow-sm">
|
||||
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">
|
||||
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="">
|
||||
<InstallCommand item={item} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">
|
||||
Location of config file
|
||||
</h2>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="">
|
||||
<ConfigFile item={item} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DefaultPassword item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function Alerts({ item }: { item: Script }) {
|
||||
<>
|
||||
{item?.notes?.length > 0 &&
|
||||
item.notes.map((note: NoteProps, index: number) => (
|
||||
<div key={index} className="mt-4 flex flex-col gap-2">
|
||||
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm",
|
||||
|
||||
@@ -1,82 +1,99 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { BookOpenText, Code, Globe, RefreshCcw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
||||
|
||||
const generateInstallSourceUrl = (slug: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/install/${slug}-install.sh`;
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/install/${slug}-install.sh`;
|
||||
};
|
||||
|
||||
const generateSourceUrl = (slug: string, type: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return type === "vm" ? `${baseUrl}/vm/${slug}.sh` : `${baseUrl}/misc/${slug}.sh`;
|
||||
return `${baseUrl}/misc/${slug}.sh`;
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return `${baseUrl}/vm/${slug}.sh`;
|
||||
case "pve":
|
||||
return `${baseUrl}/tools/pve/${slug}.sh`;
|
||||
case "addon":
|
||||
return `${baseUrl}/tools/addon/${slug}.sh`;
|
||||
default:
|
||||
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
||||
}
|
||||
};
|
||||
|
||||
const generateUpdateUrl = (slug: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/ct/${slug}.sh`;
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/ct/${slug}.sh`;
|
||||
};
|
||||
|
||||
interface ButtonLinkProps {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
interface LinkItem {
|
||||
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 isCtOrDefault = ["ct"].includes(item.type);
|
||||
const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null;
|
||||
const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null;
|
||||
const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null;
|
||||
const isCtOrDefault = ["ct"].includes(item.type);
|
||||
const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null;
|
||||
const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null;
|
||||
const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null;
|
||||
|
||||
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",
|
||||
},
|
||||
installSourceUrl && {
|
||||
href: installSourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Install-Source",
|
||||
},
|
||||
updateSourceUrl && {
|
||||
href: updateSourceUrl,
|
||||
icon: <RefreshCcw className="h-4 w-4" />,
|
||||
text: "Update-Source",
|
||||
},
|
||||
sourceUrl && {
|
||||
href: sourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Source Code",
|
||||
},
|
||||
].filter(Boolean) as ButtonLinkProps[];
|
||||
const links = [
|
||||
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",
|
||||
},
|
||||
installSourceUrl && {
|
||||
href: installSourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Install Source",
|
||||
},
|
||||
updateSourceUrl && {
|
||||
href: updateSourceUrl,
|
||||
icon: <RefreshCcw className="h-4 w-4" />,
|
||||
text: "Update Source",
|
||||
},
|
||||
sourceUrl && {
|
||||
href: sourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Source Code",
|
||||
},
|
||||
].filter(Boolean) as LinkItem[];
|
||||
|
||||
return (
|
||||
if (links.length === 0) return null;
|
||||
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{buttons.map((props, index) => (
|
||||
<ButtonLink key={index} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<LinkIcon className="size-4" />
|
||||
Links
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{links.map((link, index) => (
|
||||
<DropdownMenuItem key={index} asChild>
|
||||
<a href={link.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground size-4">{link.icon}</span>
|
||||
<span>{link.text}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import ConfigCopyButton from "@/components/ui/config-copy-button";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function ConfigFile({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="px-4 pb-4">
|
||||
<ConfigCopyButton>{item.config_path ? item.config_path : "No config path set"}</ConfigCopyButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,24 +14,19 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-lg border bg-accent/50">
|
||||
<div className="flex gap-3 px-4 py-2">
|
||||
<div className="mt-4 rounded-lg border shadow-sm">
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<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}.
|
||||
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")}
|
||||
>
|
||||
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
||||
{item.default_credentials[type as "username" | "password"]}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,29 @@
|
||||
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 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 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">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 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);
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<div className="space-y-4 flex-col flex">
|
||||
{hasDefaultSettings && <ResourceDisplay settings={defaultSettings} title="Default settings" />}
|
||||
{defaultAlpineSettings && <ResourceDisplay settings={defaultAlpineSettings} title="Default Alpine settings" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { Script } from "@/lib/types";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { AlertCircle, NotepadText } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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={cn(
|
||||
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-lg pr-4",
|
||||
AlertColors["warning"],
|
||||
)} >
|
||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||
<span>Only use for testing, not in production!</span>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground pt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{TextCopyBlock(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,85 +5,73 @@ import { Script } from "@/lib/types";
|
||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
||||
|
||||
const getInstallCommand = (scriptPath = "", isAlpine = false) => {
|
||||
const url = `https://github.com/community-scripts/${basePath}/raw/main/${scriptPath}`;
|
||||
return isAlpine
|
||||
? `bash -c "$(curl -fsSL ${url})"`
|
||||
: `bash -c "$(curl -fsSL ${url})"`;
|
||||
const url = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
|
||||
};
|
||||
|
||||
|
||||
export default function InstallCommand({ item }: { item: Script }) {
|
||||
const alpineScript = item.install_methods.find(
|
||||
(method) => method.type === "alpine",
|
||||
);
|
||||
const alpineScript = item.install_methods.find((method) => method.type === "alpine");
|
||||
|
||||
const defaultScript = item.install_methods.find(
|
||||
(method) => method.type === "default",
|
||||
);
|
||||
const defaultScript = item.install_methods.find((method) => method.type === "default");
|
||||
|
||||
const renderInstructions = (isAlpine = false) => (
|
||||
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 === "pve" ? (
|
||||
<>
|
||||
To use the {item.name} script, run the command below **only** in the Proxmox VE Shell. This script is
|
||||
intended for managing or enhancing the host system directly.
|
||||
</>
|
||||
) : item.type === "addon" ? (
|
||||
<>
|
||||
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
|
||||
Proxmox VE host to extend functionality with {item.name}.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
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 ? (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>{getInstallCommand(defaultScript.script)}</CodeCopyButton>
|
||||
</>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,39 +4,18 @@ 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-l font-semibold">
|
||||
{"Default Interface:"}
|
||||
</h2>{" "}
|
||||
|
||||
<CopyButton label="default interface" value={item.interface_port} />
|
||||
</div>
|
||||
) : null}
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{item.interface_port !== null ? (
|
||||
<div className="flex items-center justify-end">
|
||||
<h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
|
||||
<span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
|
||||
{item.interface_port}
|
||||
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,42 +4,42 @@ import type { Category, Script } from "@/lib/types";
|
||||
import ScriptAccordion from "./ScriptAccordion";
|
||||
|
||||
const Sidebar = ({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) => {
|
||||
const uniqueScripts = items.reduce((acc, category) => {
|
||||
for (const script of category.scripts) {
|
||||
if (!acc.some((s) => s.name === script.name)) {
|
||||
acc.push(script);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Script[]);
|
||||
const filteredItems = items.filter(category => category.scripts && category.scripts.length > 0);
|
||||
|
||||
const filteredItems = items.filter(category => category.scripts.length > 0);
|
||||
const uniqueScripts = filteredItems.reduce((acc, category) => {
|
||||
for (const script of category.scripts) {
|
||||
if (!acc.some((s) => s.name === script.name)) {
|
||||
acc.push(script);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Script[]);
|
||||
|
||||
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">
|
||||
{uniqueScripts.length} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={filteredItems}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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">
|
||||
{uniqueScripts.length} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={filteredItems}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
13
frontend/src/app/scripts/_components/VersionBadge.tsx
Normal file
13
frontend/src/app/scripts/_components/VersionBadge.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AppVersion } from "@/lib/types";
|
||||
|
||||
interface VersionBadgeProps {
|
||||
version: AppVersion;
|
||||
}
|
||||
|
||||
export function VersionBadge({ version }: VersionBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium text-sm">{version.version}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
import ScriptItem from "@/app/scripts/_components/ScriptItem";
|
||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -15,27 +16,35 @@ import React from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Sparkles } from "lucide-react"; // <- Hinzugefügt
|
||||
|
||||
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 <Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>;
|
||||
case "pve":
|
||||
return <Badge className="text-orange-500/75 border-orange-500/75">PVE</Badge>;
|
||||
case "addon":
|
||||
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// random Script
|
||||
function getRandomScript(categories: Category[]): Script | null {
|
||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
||||
if (allScripts.length === 0) return null;
|
||||
const idx = Math.floor(Math.random() * allScripts.length);
|
||||
return allScripts[idx];
|
||||
}
|
||||
|
||||
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);
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -45,7 +54,6 @@ export default function CommandMenu() {
|
||||
setOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
@@ -63,35 +71,65 @@ export default function CommandMenu() {
|
||||
});
|
||||
};
|
||||
|
||||
const openRandomScript = async () => {
|
||||
if (links.length === 0) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const categories = await fetchCategories();
|
||||
setLinks(categories);
|
||||
const randomScript = getRandomScript(categories);
|
||||
if (randomScript) {
|
||||
router.push(`/scripts?id=${randomScript.slug}`);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
const randomScript = getRandomScript(links);
|
||||
if (randomScript) {
|
||||
router.push(`/scripts?id=${randomScript.slug}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={openRandomScript}
|
||||
title="Open random script"
|
||||
disabled={isLoading}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
||||
{links.map((category) => (
|
||||
<CommandGroup
|
||||
key={`category:${category.name}`}
|
||||
heading={category.name}
|
||||
>
|
||||
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
||||
{category.scripts.map((script) => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
@@ -104,10 +142,7 @@ export default function CommandMenu() {
|
||||
<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`)
|
||||
}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
|
||||
29
frontend/src/components/FAQ.tsx
Normal file
29
frontend/src/components/FAQ.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { Plus } from "lucide-react";
|
||||
import { FAQ_Items } from "../config/faqConfig";
|
||||
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{FAQ_Items.map((item, index) => (
|
||||
<AccordionItem value={index.toString()} key={index} className="py-2">
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center gap-3 py-2 text-left text-[15px] font-semibold leading-6 transition-all [&>svg>path:last-child]:origin-center [&>svg>path:last-child]:transition-all [&>svg>path:last-child]:duration-200 [&>svg]:-order-1 [&[data-state=open]>svg>path:last-child]:rotate-90 [&[data-state=open]>svg>path:last-child]:opacity-0 [&[data-state=open]>svg]:rotate-180">
|
||||
{item.title}
|
||||
<Plus
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
className="shrink-0 opacity-60 transition-transform duration-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="pb-2 ps-7 text-muted-foreground">{item.content}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,13 +6,13 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-6 backdrop-blur-lg">
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg">
|
||||
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Website built by the community. The source code is available on{" "}
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
href={`https://github.com/community-scripts/${basePath}/frontend`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-semibold underline-offset-2 duration-300 hover:underline"
|
||||
|
||||
48
frontend/src/components/icons/resource-icons.tsx
Normal file
48
frontend/src/components/icons/resource-icons.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
export function CPUIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="9" y="9" width="6" height="6" />
|
||||
<path d="M3 9h2m14 0h2M3 15h2m14 0h2M9 3v2m6-2v2M9 19v2m6-2v2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function RAMIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<rect x="4" y="6" width="16" height="12" rx="2" ry="2" />
|
||||
<path d="M8 6v12M16 6v12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function HDDIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path d="M4 4h16v16H4z" />
|
||||
<circle cx="8" cy="16" r="1" />
|
||||
<circle cx="16" cy="16" r="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
9
frontend/src/components/query-provider.tsx
Normal file
9
frontend/src/components/query-provider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
55
frontend/src/components/ui/config-copy-button.tsx
Normal file
55
frontend/src/components/ui/config-copy-button.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"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);
|
||||
|
||||
|
||||
// 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 Config File Path"}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
37
frontend/src/config/faqConfig.tsx
Normal file
37
frontend/src/config/faqConfig.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
export const FAQ_Items = [
|
||||
{
|
||||
title: "Why do you use tarballs instead of git pull for installation?",
|
||||
content:
|
||||
"Our LXC scripts install applications using release tarballs. Tarballs contain stable code versions tested for release. Using `git pull` directly fetches the latest development code, which might be unstable or contain bugs. Tarballs offer a more reliable installation.",
|
||||
},
|
||||
{
|
||||
title: "Why do the scripts install applications using HTTP by default?",
|
||||
content:
|
||||
"Our LXC scripts install applications using HTTP by default. Setting up HTTPS often requires manual configuration specific to your setup, which our automated scripts cannot handle reliably. If an application requires HTTPS, the script will configure it. For others, you need to enable HTTPS yourself, often following the application's official documentation.",
|
||||
},
|
||||
{
|
||||
title: "Where can I find documentation for the installed application?",
|
||||
content:
|
||||
"We link to the official documentation for each application whenever possible. You can usually find this link on the script's information page for the specific LXC script. If you notice a missing link for an application that has official docs, please report it so we can add it.",
|
||||
},
|
||||
{
|
||||
title: "What should I do if I find a bug in an LXC script?",
|
||||
content:
|
||||
"Our LXC scripts are maintained by volunteers in their free time. Bugs can occur due to our errors or changes in the applications themselves. If you find a bug in one of our LXC scripts, please report it on our GitHub issues page. Your feedback helps us improve the scripts.",
|
||||
},
|
||||
{
|
||||
title: "Why isn't the application updating to the very latest version?",
|
||||
content:
|
||||
"Updates via our LXC scripts might not pull the absolute latest version for a few reasons:\n- A bug in the application's release naming on GitHub.\n- A bug in our update script.\n- We intentionally pinned the version. This happens if a newer version has breaking changes or serious bugs that could affect your data or LXC stability. We wait for fixes before allowing the update.",
|
||||
},
|
||||
{
|
||||
title: 'Why am I getting a "502 Bad Gateway" error?',
|
||||
content:
|
||||
'A "502 Bad Gateway" error usually means the application inside the LXC is not running or responding correctly. Check the application\'s logs first. If you use a reverse proxy, check its logs too. If you still have problems after checking the logs, report the issue, providing details from the logs.',
|
||||
},
|
||||
{
|
||||
title: "What should I do if a script fails during execution?",
|
||||
content:
|
||||
"If an LXC script fails, run it again using Verbose mode. Standard mode hides detailed output for neatness, showing only progress. Verbose mode displays all messages, which helps you (and us) diagnose the error. Include this verbose output if you report the issue.",
|
||||
},
|
||||
];
|
||||
@@ -46,7 +46,7 @@ export const mostPopularScripts = ["post-pve-install", "docker", "homeassistant"
|
||||
|
||||
export const analytics = {
|
||||
url: "analytics.proxmoxve-scripts.com",
|
||||
token: "b60d3032-1a11-4244-a100-81d26c5c49a7",
|
||||
token: "aefee1b9-2a12-4ac2-9d82-a63113edc62e",
|
||||
};
|
||||
|
||||
export const AlertColors = {
|
||||
|
||||
21
frontend/src/hooks/useVersions.ts
Normal file
21
frontend/src/hooks/useVersions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { fetchVersions } from "@/lib/data";
|
||||
import { AppVersion } from "@/lib/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export function useVersions() {
|
||||
return useQuery<AppVersion[]>({
|
||||
queryKey: ["versions"],
|
||||
queryFn: async () => {
|
||||
const fetchedVersions = await fetchVersions();
|
||||
if (Array.isArray(fetchedVersions)) {
|
||||
return fetchedVersions;
|
||||
}
|
||||
if (fetchedVersions && typeof fetchedVersions === "object") {
|
||||
return [fetchedVersions];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -10,9 +10,9 @@ export const fetchCategories = async () => {
|
||||
};
|
||||
|
||||
export const fetchVersions = async () => {
|
||||
const response = await fetch(`api/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch versions: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
const response = await fetch(`api/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch versions: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export type Script = {
|
||||
slug: string;
|
||||
categories: number[];
|
||||
date_created: string;
|
||||
type: "vm" | "ct" | "misc";
|
||||
type: "vm" | "ct" | "pve" | "addon";
|
||||
updateable: boolean;
|
||||
privileged: boolean;
|
||||
interface_port: number | null;
|
||||
@@ -13,7 +13,7 @@ export type Script = {
|
||||
website: string | null;
|
||||
logo: string | null;
|
||||
description: string;
|
||||
version: string;
|
||||
config_path: string | null;
|
||||
install_methods: {
|
||||
type: "default" | "alpine";
|
||||
script: string;
|
||||
@@ -48,12 +48,6 @@ export type Metadata = {
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
export interface AppVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
name: string;
|
||||
slug: string;
|
||||
@@ -63,3 +57,9 @@ export interface OperatingSystem {
|
||||
name: string;
|
||||
versions: Version[];
|
||||
}
|
||||
|
||||
export interface AppVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
7
frontend/src/lib/utils/resource-utils.ts
Normal file
7
frontend/src/lib/utils/resource-utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function getDisplayValueFromRAM(ram: number): string {
|
||||
return ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`;
|
||||
}
|
||||
|
||||
export function cleanSlug(slug: string): string {
|
||||
return slug.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
||||
}
|
||||
Reference in New Issue
Block a user